Skip to content

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.

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.

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”

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.

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.

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.

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”.

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.