REST API conventions
- Written for
- + Written for
- Deprecated
- + Deprecated
- Applies to
- + Applies to
REST API conventions
These conventions apply to every endpoint under https://shippified.net/api.
Base URL
https://shippified.net/apiAll paths in these docs include the /api prefix, as in GET /api/orders → https://shippified.net/api/orders. Only HTTPS is supported.
Requests
Methods. The API uses
GET,POST,PATCH, andDELETE. There is noPUT.Bodies are JSON. Send
Content-Type: application/json. The server parses the body as JSON whether or not the header is set.Invalid JSON is not rejected up front. A body that is not valid JSON is kept as a raw string. The endpoint then sees missing fields and usually returns
400with a field-specific message.Body size limit: 2 MiB. A larger body gets
413with{"error": "Request body too large (limit 2 MiB)."}. The server reads and discards the rest of a moderately oversized upload so your client receives the413instead of a broken connection. For very large uploads it answers and closes the connection.Prototype keys are stripped. Keys named
__proto__,prototype, orconstructorare dropped at any depth.Unknown fields are ignored. Each endpoint reads only the fields it documents.
Query parameters are used for filters and pagination on
GETrequests, as in?status=shipped&limit=100.
Responses
Every response from the JSON API has Content-Type: application/json; charset=utf-8. Most endpoints return the resource itself. A few return a wrapper, such as { "order": …, "merged": … } from POST /api/orders. Each endpoint page shows its exact shape.
Fields with no value are left out of the response. They are not sent as null. For example, an order without tracking has no trackingNumber key at all.
Errors
Errors use one shape:
{ "error": "Human-readable message" }Some errors add machine-readable fields next to error:
Extra field(s) | Returned by | Meaning |
|---|---|---|
|
| Monthly plan limit reached. |
| Inbound mail (402) | Monthly plan limit reached. Here |
|
| The real sender (after unwrapping a manually forwarded email) is not in the source's allowed senders. Missing when the email has no sender at all. |
Use the status code to drive program logic. The wording of error messages can change.
Status codes
Code | Used for |
|---|---|
| Successful reads, updates, deletes ( |
| A resource was created: orders, API keys, webhook subscriptions, bots, email sources, custom templates, shares. |
| Intake endpoints that process a message: Discord intake, |
| CORS preflight ( |
| Missing or invalid input, for example |
| Missing, invalid, revoked, or expired credentials. Also returned for an unknown handle on Discord intake, and when an API key is used on an endpoint that needs a signed-in session (account export and deletion). |
| Inbound mail only: plan limit reached. |
|
|
| The resource doesn't exist or belongs to another account. Unknown |
| Username taken, email already registered, or a mailbox rebuild already running. |
| Request body over 2 MiB. |
|
|
| Rate limit hit, resource cap reached (25 API keys, 25 webhook subscriptions), or monthly plan limit reached on a create. |
| Unhandled error. The body is |
| A dependency is not configured on this deployment (for example Google sign-in, webhook secret encryption, or the inbound mail key), or |
Note: Resources owned by another account return
404, never403. Every read and write is scoped to the authenticated account.
Pagination
Collection endpoints that can grow large use offset pagination and a shared envelope.
Query parameter | Default | Bounds | Notes |
|---|---|---|---|
|
| 1 to 200 | Values above 200 are capped at 200. Non-numeric values or |
|
| ≥ 0 | Number of items to skip. |
Response envelope:
{
"items": [],
"total": 342,
"limit": 50,
"offset": 100,
"hasMore": true
}Field | Meaning |
|---|---|
| The page of results. |
| Total matches after filters, before pagination. |
| The values the server applied, after clamping. |
|
|
Endpoints that use this envelope:
Endpoint | Default sort |
|---|---|
|
|
| Not guaranteed (typically newest first) |
| Not guaranteed (typically newest first) |
| Not guaranteed (typically newest first) |
|
|
Walking 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))
doneNote: Pagination is offset-based over a list sorted newest first. If orders arrive while you are paging, items can shift between pages, so you may see some twice. Deduplicate by
idwhen walking a live list.
A few smaller lists are not paginated and use their own wrapper:
Endpoint | Shape | Limit |
|---|---|---|
|
| At most 25 keys exist. |
|
| None. |
|
|
|
|
|
|
|
|
|
|
| None. Newest first, which is also the order custom templates are tried in. |
Rate limits and caps
Only the endpoints below are rate-limited. Limits use a sliding 60-second window and are tracked in memory per server process.
Scope | Limit | Over-limit response |
|---|---|---|
Auth: | 10 requests per minute per client IP |
|
Discord intake, per client IP (all bots combined) | 60 requests per minute |
|
Discord intake, per bot URL | 60 requests per minute |
|
The client IP is the leftmost X-Forwarded-For entry, or the socket address when that header is absent.
Resource caps:
Resource | Cap | Response when exceeded |
|---|---|---|
API keys per account | 25 |
|
Webhook subscriptions per account | 25 |
|
New orders per month on the free plan | 100 (only when billing is enabled on the deployment) |
|
The plan limit applies only to creating new orders. Merges into existing orders, such as status-update emails, are never blocked. GET /api/billing/usage returns the current plan, used, cap, and resetsAt.
Idempotency
The API does not support an Idempotency-Key header. Duplicate protection comes from the data model instead:
Path | Dedup rule |
|---|---|
| If |
Email intake (import, polling, forwarding) | Deduplicated by |
Discord intake | Not deduplicated. Discord payloads have no message ID, and two identical checkouts can be two real orders. Posts that share an order number still merge. |
Important: Before retrying a failed
POST /api/orders, include anorderNumberso a retry merges rather than duplicates. OrGET /api/orders?q=first to check whether the order already exists.
IDs
IDs are opaque strings with a type prefix. Don't parse them or rely on how long they are.
Resource | Prefix | Example |
|---|---|---|
Order |
|
|
User |
|
|
API key |
|
|
Webhook subscription |
|
|
Webhook event (delivery envelope) |
|
|
Bot |
|
|
Email source |
|
|
Custom template |
|
|
Recurring cost |
|
|
Intake log entry |
|
|
Some values are secrets rather than IDs. API keys start with sk_ and webhook signing secrets start with whsec_.
Timestamps, dates, and money
Kind | Format | Example | Fields |
|---|---|---|---|
Timestamp | ISO 8601 UTC with milliseconds |
|
|
Calendar date |
|
|
|
Money | Integer cents |
|
|
Display money | Free-text string as parsed |
| Order |
Note:
totalandpriceare strings copied from the source message. Do arithmetic oncostCentsandsalePriceCentsonly.
CORS
Every JSON response includes:
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET,POST,PATCH,DELETE,OPTIONS
Access-Control-Allow-Headers: authorization,content-typeOPTIONS requests to any path get 204 with no body. By default the allowed origin is *. A deployment can restrict it to a list of origins through server configuration. Authentication uses the Authorization header, not cookies, so credentialed-CORS rules do not apply.
Important: CORS lets a browser call the API, but you should not put an API key in browser code. A key grants full access to the workspace. Proxy calls through your own backend.
Public endpoints
These endpoints require no token:
Endpoint | Purpose |
|---|---|
| Health check with uptime, version, persistence status, and counts. Returns |
| The OpenAPI 3.1 spec. |
| Interactive API reference (HTML). |
| Aggregate platform stats. |
| A public profile, when the owner has made it public. |
| Operator-only drop-off for the email worker. It is not open: it needs the deployment's inbound key and returns |
| Discord intake. The URL is the credential. See Getting orders in. |
Workspace snapshot
GET /api/state returns the whole workspace in one response: user, settings, bots (each with stats and recentOrders), emailSources, orders (all of them), analytics, templates (built-in and custom), and subscriptions. The dashboard uses it. Integrations should prefer the paginated resource endpoints, because this response grows with the size of the workspace.