📐REST API conventions

Base URL, request and response rules, errors, pagination, rate limits, IDs and dates that apply to every endpoint.

Written for
Developers calling the Shippified API
Applies to
All plans
AdminUpdated Sep 26, 2026

These rules apply to every endpoint under https://shippified.net/api. Read this page once; the resource pages assume it.

Base URL

https://shippified.net/api

Paths in these docs include the /api prefix: GET /api/orders means https://shippified.net/api/orders. Every request except the public endpoints needs an Authorization: Bearer header; see Authentication.

Requests

  • Methods. GET, POST, PATCH and DELETE. There is no PUT.

  • JSON bodies. Send Content-Type: application/json. The server parses the body as JSON either way.

  • Invalid JSON isn't rejected up front. A body that doesn't parse is kept as a raw string, so the endpoint sees its fields as missing and usually answers 400 with a field-specific message such as itemSummary is required.

  • 2 MiB body limit. A larger body gets 413 {"error": "Request body too large (limit 2 MiB)."}. For moderately oversized uploads the server reads and discards the rest first, so your client sees the 413 instead of a broken connection. Very large uploads get the 413 and a closed connection.

  • Unknown fields are ignored. Each endpoint reads only the fields it documents. Keys named __proto__, prototype or constructor are stripped at any depth.

  • Filters and paging go in the query string: ?status=shipped&limit=100.

Responses

Every JSON response has Content-Type: application/json; charset=utf-8. Most endpoints return the resource itself; a few wrap it, such as POST /api/orders → { "order": …, "merged": … }. Each resource page shows the exact shape.

Fields without a value are left out, not sent as null. An order with no tracking number simply has no trackingNumber key.

Errors

Every error has the same shape:

{ "error": "Human-readable message" }

Some add machine-readable fields next to error:

Extra fields

Returned by

Meaning

reason: "plan_limit_reached", cap, used

POST /api/orders and POST /api/email/import (429), Discord intake (202 with rejected: true)

The free plan's monthly order limit is used up.

blockedSender

POST /api/email/import (403)

The email's real sender isn't in the source's allowed senders. Absent when the email has no sender at all.

reason: "email_unverified"

POST /api/billing/checkout (403)

Verify the account email before upgrading.

code

POST /api/auth/atlas-login

A short code for the sign-in failure.

Branch on the status code (and reason where present), not on the wording of error. Messages are written for people and can change.

Status codes

Code

Used for

200 OK

Reads, updates, deletes ({"ok": true}) and actions.

201 Created

A resource was created: orders, API keys, webhook subscriptions, bots, email sources, custom templates, recurring costs, shares.

202 Accepted

Intake that processes a message (Discord intake, POST /api/email/import) and the background mailbox rebuild.

204 No Content

CORS preflight (OPTIONS) responses. No body.

400 Bad Request

Missing or invalid input: itemSummary is required, a bad receivedAt or eventType, an invalid regex in a template, a webhook URL that points at a private address, sinceDays must be 1–365..

401 Unauthorized

Missing, invalid, revoked or expired credentials, or an API key on an endpoint that needs a signed-in session.

403 Forbidden

Only for two rules: an email sender blocked by the allow-list, and upgrading before the email is verified. Resources from other accounts return 404, not 403.

404 Not Found

The resource doesn't exist or belongs to another account. Unknown /api routes return {"error": "Not found"}.

409 Conflict

Username taken, email already registered, or a mailbox rebuild already running.

413 Content Too Large

Body over 2 MiB.

422 Unprocessable Content

POST /api/email/import: the message couldn't be read as an email order.

429 Too Many Requests

A rate limit, a resource cap (25 API keys, 25 webhook subscriptions), or the monthly plan limit on a create.

500 Internal Server Error

Unhandled error. The body is still {"error": "<message>"}.

502 Bad Gateway

An upstream service (the sign-in service, Discord) couldn't be reached.

503 Service Unavailable

A feature isn't configured on this deployment (for example Google sign-in, billing, or webhook-secret encryption), or /api/healthz found storage unhealthy.

Pagination

Collections that can grow large use offset pagination and one envelope.

Query parameter

Default

Bounds

Notes

limit

50

1 to 200

Above 200 is capped at 200. 0, negative or non-numeric values fall back to 50 (negative becomes 1).

offset

0

0 or more

Items to skip. Non-numeric values become 0.

{
  "items": [ … ],
  "total": 342,
  "limit": 50,
  "offset": 100,
  "hasMore": true
}

Field

Meaning

items

This page.

total

All matches after filters, before paging.

limit, offset

The values the server actually used, after clamping.

hasMore

true when offset + items.length < total.

Endpoints that use this envelope:

Endpoint

Order

GET /api/orders

receivedAt, newest first

GET /api/webhook-subscriptions

createdAt, newest first

GET /api/bots

Not guaranteed

GET /api/email-sources

Not guaranteed

GET /api/subscriptions

Not guaranteed

Walk every page

offset=0
while :; do
  page=$(curl -s "https://shippified.net/api/orders?limit=200&offset=$offset" \
    -H "Authorization: Bearer $SHIPPIFIED_API_KEY")
  echo "$page" | jq -c '.items[]'
  [ "$(echo "$page" | jq -r .hasMore)" = "true" ] || break
  offset=$((offset + 200))
done

Pages are cut from a list sorted newest first. If orders arrive while you page, items shift and you can see some twice. Deduplicate by id.

A few small lists aren't paginated and use their own wrapper:

Endpoint

Shape

Limit

GET /api/account/api-keys

{ items }

At most 25 keys exist.

GET /api/templates/custom

{ items }

None. Newest first, which is also the order templates are tried in.

GET /api/shares

{ shares }

None.

GET /api/webhook-logs

{ logs }

?limit= 1 to 500, default 100. Newest first.

GET /api/bots/:id/logs

{ logs }

?limit= 1 to 500, default 200. Newest first.

GET /api/inbox

{ items, total }

?limit= 1 to 500, default 200. Optional ?sourceId=.

Rate limits and caps

Only the endpoints below are rate-limited. Each limit is a sliding 60-second window counted in the server's memory.

Scope

Limit

Over the limit

Sign-in endpoints: POST /api/auth/register, /login, /forgot-password, /reset-password, /atlas-login, /discord/widget-verify

10 a minute per client IP, shared

429 with Retry-After: 60

Discord intake, per client IP across all bots

60 a minute

429, Retry-After: 60, {"error": "Webhook ingest rate exceeded (60/min per IP)"}

Discord intake, per bot URL

60 a minute

429, Retry-After: 60, {"error": "Webhook ingest rate exceeded for this bot (60/min)"}

Embed copier submission URL, per workspace

30 a minute

429 {"error": "Too many submissions — slow down."}

The client IP is the first address in X-Forwarded-For, or the connection's address when that header is absent.

Resource caps:

Resource

Cap

When exceeded

API keys per account

25

429

Webhook subscriptions per account

25

429

New orders a month on the free plan

100, counted from the 1st of the month in your account's timezone. Applies only when billing is switched on for the deployment.

429 on POST /api/orders and POST /api/email/import; 202 with rejected: true on Discord intake

The plan limit counts only new orders. Messages that merge into an existing order (shipping and delivery emails, for example) are never blocked. GET /api/billing/usage returns plan, used, cap (null means unlimited), resetsAt and billingEnabled.

Duplicates and retries

There's no Idempotency-Key header. Duplicates are prevented by the data instead:

Path

Rule

POST /api/orders

An orderNumber that matches an existing order merges into it (merged: true). Without an orderNumber, every call creates a new order.

Email (import, polling, forwarding)

Deduplicated by Message-ID, or a SHA-256 of the raw message when there's none. POST /api/email/import answers 200 with duplicate: true for a repeat.

Discord intake

Not deduplicated. Discord payloads carry no message ID, and two identical checkouts can be two real orders. Posts with the same order number still merge.

Before retrying a failed POST /api/orders, make sure the body has an orderNumber, so a retry merges instead of duplicating. Or search first with GET /api/orders?q=<order number>.

IDs

IDs are opaque strings with a type prefix. Don't parse them or rely on their length.

Resource

Prefix

Example

Order

ord_

ord_mpvu9uim_aeh5t

User

usr_

usr_4f1c2a9b7e0d3c11

API key

key_

key_m1x2y3z4_a1b2c3

Bot

bot_

bot_m1x2y3z4

Email source

email_

email_m1x2y3z4

Custom template

tmpl_

tmpl_m1x2y3z4

Recurring cost

sub_

sub_m1x2y3z4

Webhook subscription

whsub_

whsub_m1x2y3z4_k9d2q

Webhook event

evt_

evt_m1x2y3z4_0a1b2c3d

Intake log entry

log_

log_m1x2y3z4_x7k2p

Secrets look similar but aren't IDs: API keys start with sk_ and webhook signing secrets with whsec_.

Dates and money

Kind

Format

Example

Fields

Timestamp

ISO 8601, UTC, milliseconds

2026-09-24T15:02:11.482Z

receivedAt, updatedAt, createdAt, lastUsedAt, lastFiredAt

Calendar date

YYYY-MM-DD

2026-09-27

estimatedDelivery, actualDelivery, the from/to order filters

Money

Integer cents

49999 = $499.99

costCents, salePriceCents, recurring-cost costCents

Display money

Text as it appeared in the message

"$499.99"

Order total, price

total and price are strings copied from the source message. Do arithmetic only on the …Cents fields.

CORS

JSON responses carry:

Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET,POST,PATCH,DELETE,OPTIONS
Access-Control-Allow-Headers: authorization,content-type

OPTIONS to any path gets 204 with no body. Authentication uses the Authorization header, not cookies.

CORS lets a browser call the API, but never put an API key in a web page: it grants full access to the workspace. Call Shippified from your own backend.

Public endpoints

These need no token:

Endpoint

Returns

GET /api/healthz

{ ok, uptimeMs, version, node, persistence, counts }. 503 when storage is unhealthy. Never cached.

GET /api/openapi.json

The OpenAPI 3.1 spec.

GET /api

An interactive API reference page (HTML).

GET /api/landing-stats

{ generatedAt, totalValueCents, ordersTracked, platformsSupported } across the whole platform.

GET /api/profile/:usernameOrUserId

A public profile. 404 {"error": "Profile not found"} when it's private, unless you're the owner.

GET /api/auth/providers

Which sign-in methods the deployment has switched on.

POST /api/webhooks/discord/:handle/:slug

Discord intake. The URL is the credential. See Getting orders in.

Endpoint map

Everything else needs a token. The resource pages cover the first group in depth; the rest are summarised here and fully specified in the API reference.

Area

Endpoints

Documented in

Orders and tracking

/api/orders…, /api/carrier-status

Orders

Bots, email sources, import, logs, inbox

/api/bots…, /api/email-sources…, /api/email/import, /api/webhook-logs, /api/inbox…

Getting orders in

Templates

/api/templates…

Templates API

Outbound webhooks

/api/webhook-subscriptions…

Webhooks

API keys, export, deletion

/api/account/api-keys…, /api/account/export, DELETE /api/account

Authentication

Workspace snapshot

GET /api/state

Below

Recurring costs

GET/POST /api/subscriptions, GET/PATCH/DELETE /api/subscriptions/:id. PATCH checks fields like create does and answers 400 for an empty name, a negative costCents or an unknown frequency.

Subscriptions reference

Insights

GET /api/profitability?range=month|quarter|year|all, GET /api/calendar?month=YYYY-MM, GET /api/leaderboard?timeframe=week|month|all

Insights reference

Share cards

GET/POST /api/shares, DELETE /api/shares/:slug

Shares reference

Workspace settings

POST /api/settings (outputWebhook, updatesWebhook, forwardOn)

Settings reference

Account profile

POST /api/account/username, /display-name, /visibility, /public-stats, /timezone; GET /api/account/username/available?u=

Account reference

Billing

GET /api/billing/usage, POST /api/billing/checkout, POST /api/billing/portal

Billing reference

Embed copier

GET /api/embed-copier/config, POST /api/embed-copier/rotate, /api/embed-samples…

Embed Copier reference

Discord bot

GET /api/discord/install, PATCH /api/discord/settings, POST /api/discord/digest

Discord reference

Workspace snapshot

GET /api/state returns the whole workspace in one response: user, settings, bots (each with stats and recentOrders), emailSources, every order, analytics, templates (built-in and custom) and subscriptions. The dashboard uses it. Integrations should prefer the paginated endpoints: this response grows with the workspace.

Troubleshooting

Every request returns 401

The header must be exactly Authorization: Bearer <token>, with the full key including sk_. Check the key is still listed under Settings → Developer. See Authentication.

My POST fields are ignored or I get "…is required"

Check the body is valid JSON and sent with Content-Type: application/json. Invalid JSON arrives as a plain string, so every field looks missing. Field names are case-sensitive (itemSummary, not item_summary).

A status filter returns everything

GET /api/orders ignores a status value it doesn't recognise instead of failing. Use one of ordered, shipped, delivered, canceled, issue.

I get 404 for an ID I can see in the dashboard

The token belongs to a different account than the one you're signed in to. Every read is scoped to the token's account, and other accounts' resources return 404.

Was this page helpful?