Errors
Every error this API returns is an RFC 9457 problem document, served as
Content-Type: application/problem+json. That includes the 401 from the authorizer and the
429 from the throttle, which are produced by the gateway rather than by application code but
are shaped identically - so one parser and one branch on code handles the whole API. The
sole exception is the edge firewall’s 403, described at the end.
{ "type": "about:blank", "title": "Bad Request", "status": 400, "code": "invalid_cursor", "detail": "The \"cursor\" query parameter is malformed.", "requestId": "8f3c1e2a-3d4b-4a1e-9f77-2b6c5e0a1d33"}| Field | Notes |
|---|---|
type |
Always the literal string about:blank today. Reserved by RFC 9457 for a future problem-type URI. |
title |
Short human summary of the status, for example Not Found. Derived from status; carries no information status does not. |
status |
The HTTP status code, repeated in the body. |
code |
The stable, machine-readable identifier. This is the field to branch on. |
detail |
Human-readable prose. May be reworded at any time. Do not parse it. |
requestId |
Echoes the request’s id. Include it when you escalate a specific failure. |
Codes the service returns
Section titled “Codes the service returns”Fix and resend means retrying the identical request will produce the identical response. Retry means the request was fine and the condition is transient. Back off means retry, but not immediately.
code |
Status | What happened | Action |
|---|---|---|---|
invalid_cursor |
400 | The cursor you sent could not be decoded. Usually truncated, edited, or carried over from a different query shape. |
Fix and resend Restart pagination with no cursor. Do not resend the same value. |
invalid_site_id |
400 | The siteId filter is not a positive integer, or it names a site outside your grant. |
Fix and resend Check GET /v1/me’s grantedSiteIds. |
invalid_order_number |
400 | The order number in the path is not a positive integer. | Fix and resend |
updated_since_not_supported |
400 | You sent an updatedSince parameter. It is rejected rather than ignored, on purpose. |
Fix and resend See why below. |
invalid_token |
401 | Your credential is missing, expired, or otherwise not accepted. Produced by the authorizer, so detail is generic. |
Mint a fresh token and retry once. If that fails too, the credential needs attention. |
insufficient_scope |
403 | Your credential is valid but its effective scopes do not include the one this operation requires. | Fix and resend Mint a token carrying the scope, or ask Partner Integrations to add the capability. |
order_not_found |
404 | No such order, or an order that exists outside your granted sites. These are indistinguishable by design. | Fix and resend Do not treat as proof of non-existence. |
rate_limit_exceeded |
429 | Your per-partner rate, burst, or daily quota. Carries Retry-After. |
Back off See Rate limits and quotas. |
internal_error |
500 | An unexpected server-side failure. detail is always the fixed string An unexpected error occurred. and never echoes the underlying error. |
Retry once, well spaced. If it persists, escalate with the requestId. |
service_unavailable |
503 | A transient failure in the service or the database it reads from. | Back off Exponential, starting around one second. |
This is the complete set. A test in the service compares this contract to the codes the handlers actually construct, so a code cannot be added on one side and forgotten on the other. New codes can still appear inside v1 - treat one you do not recognize as “unhandled error for this status” rather than failing on it.
Where the 401 and the 429 come from
Section titled “Where the 401 and the 429 come from”Both are produced by the API gateway, before your request reaches any application code: the
401 by the authorizer, the 429 by your usage plan. They are nonetheless shaped as the same
problem document as everything else, with the same fields and a requestId, so you do not
need a second code path for them.
The 401 is identical for every cause - no header, a malformed one, an expired token, a
disabled credential - because a 401 that explained which check failed would help someone
probing with guessed values more than it helps you. See
when authentication fails.
The one response that is not a problem document
Section titled “The one response that is not a problem document”403, from the edge
Section titled “403, from the edge”The edge web application firewall enforces a per-IP rate-based rule and a set of managed rule
sets. When either blocks a request the response is a 403 from the firewall: no problem
document, and unrelated to your token’s scopes.
This is the one case in this API where a 403 does not mean “your credential lacks a
capability”. If you are seeing 403 without an insufficient_scope body, look at your
request rate and your source IP before you look at your scopes. See
Rate limits and quotas.
Retry policy
Section titled “Retry policy”A summary you can encode directly:
| Status | Retry? | How |
|---|---|---|
| 400 | No | The request must change. Retrying it unchanged returns the same answer forever. |
| 401 | Once | Mint a fresh token, retry once. Then stop and investigate the credential. |
| 403 | No | Either a missing scope (fix the token) or the edge firewall (slow down). Neither is fixed by an immediate retry. |
| 404 | No | |
| 429 | Yes | Honor Retry-After, and add exponential backoff with jitter on top of it. |
| 500 | Cautiously | Once, well spaced. A persistent 500 is a bug on our side and needs the requestId, not more traffic. |
| 503 | Yes | Exponential backoff from around one second. |
Cap your retries and add jitter. A fleet of integrations retrying a transient failure in lockstep turns a blip into an outage, and the 503 path exists precisely for moments when the service has less capacity than usual.
Two rules worth internalising
Section titled “Two rules worth internalising”404 is not proof of non-existence
Section titled “404 is not proof of non-existence”order_not_found means “no order matching this request is visible to you”. It does not
distinguish “this order number was never issued” from “this order belongs to a site you are
not granted”, and it never will - confirming existence is itself a disclosure. Do not write
logic that treats a 404 from this API as grounds to mark a local record cancelled, deleted,
or invalid. See Tenancy and access.
No change polling
Section titled “No change polling”updatedSince is rejected with a 400 rather than accepted and ignored, because a filter you
believe is working is how you end up trusting an incomplete feed. The updatedOn field is
not a usable watermark either: it does not move when a shipment posts, a tracking number is
added, or item quantities change on an existing order, and a separate internal process can
move an order into your site without touching it.
GET /v1/orders ordered by placedOn discovers new orders and nothing else. What is missing
is only the ability to ask which orders changed: fetching one order you already know about
always returns current state, so re-requesting GET /v1/orders/{orderNumber}/shipments on your
own schedule is the supported way to watch for tracking. See
Tracking and line items.
A 404 on a sub-resource is about the order, not the collection
Section titled “A 404 on a sub-resource is about the order, not the collection”GET /v1/orders/{orderNumber}/shipments and .../items return 200 with an empty array when
the order resolves for you and has nothing to show yet. A 404 from either means the order
number itself does not resolve for you. Do not collapse the two: the first means “not yet”, the
second means “wrong order number, or not your site”.
Escalating
Section titled “Escalating”Include the requestId from the failing response. It resolves to a single log entry, which
is dramatically faster than a search by approximate timestamp. The only failure with no
requestId to send is the edge 403, which never reaches our logs at all - for that one,
include the exact time with a timezone, the host you called, and your source IP instead.