Errors
When a request fails, the body is an error envelope:
{ "error": { "code": "NOT_FOUND", "message": "Purchase order not found", "retryable": false, "requestId": "0f8c…" }}Retryable errors also include retryAfterMs and a Retry-After response header
(in seconds). Many errors also carry an action field — a short, human-readable next
step. Read it: on write endpoints it usually tells you exactly what to fix.
Status codes
Section titled “Status codes”code | HTTP | Meaning | Retry-After |
|---|---|---|---|
UNAUTHORIZED | 401 | Missing, invalid, or revoked token | — |
PLAN_REQUIRED | 403 | The shop isn’t on the Elevate plan (the REST API is Elevate-only) | — |
FORBIDDEN_SCOPE | 403 | The token’s scope can’t do this — e.g. a read key on a write endpoint | — |
INVALID_INPUT | 400 | A bad parameter, a bad body, or a change the order’s current state doesn’t allow (the message names it) | — |
NOT_FOUND | 404 | Unknown path, or the id doesn’t exist | — |
RATE_LIMITED | 429 | Per-shop rate limit reached | ✓ |
DO_OVERLOADED | 503 | The shop’s data store is busy | ✓ |
DO_UNAVAILABLE | 503 | The data store is temporarily unavailable | ✓ |
UPSTREAM_SHOPIFY | 502 | A Shopify-facing dependency failed | if present |
TIMEOUT | 504 | The request timed out | if present |
OUTPUT_TOO_LARGE | 413 | The response exceeded the size cap | — |
INTERNAL | 500 | An unexpected error | — |
Sending a method an endpoint doesn’t support returns 405 (METHOD_NOT_ALLOWED) with an
Allow header.
400 on writes: what the order wouldn’t let you do
Section titled “400 on writes: what the order wouldn’t let you do”Most write failures aren’t malformed requests — they’re changes the purchase order’s current state doesn’t permit. The guards are the same ones the app itself uses, so the message describes the real rule:
- “Completed” is never a target for
/status— usePOST …/complete. - A move that isn’t allowed from the current status — the message lists the ones that are. See Purchase-order lifecycle.
- Draft → Sent without a location — set
locationIdfirst withPATCH …/{id}. /completeon an order that isn’t Received or Partially Received — receive first, or usePOST …/quick-complete.- Receiving less than has already been synced to Shopify — use
POST …/unreceive. - A stock sync is still in flight (
ON_HAND_SYNC_IN_FLIGHT) — wait a moment and retry the same call. - Variants that aren’t active in Shopify (
INVENTORY_GUARD_ISSUES) — resend the same call with aresolutionsarray saying what to do with each one. See the lifecycle page.
502 on writes: Shopify said no
Section titled “502 on writes: Shopify said no”502 UPSTREAM_SHOPIFY means a Shopify-side call failed while your request was running.
On POST …/complete and POST …/quick-complete this matters, because it tells you where
things stopped:
/complete— the cost sync runs first, so a502leaves the order’s status unchanged. Cost updates that already succeeded are kept and skipped when you retry./quick-complete— a total sync failure leaves the quantities already received but the order not completed. Recover withPOST …/shopify-sync/resetfollowed byPOST …/shopify-sync, or simply retry/quick-complete.
Either way, re-read the order with GET /api/v1/purchase-orders/{id} before deciding.
The failure that arrives inside a 200
Section titled “The failure that arrives inside a 200”Write endpoints save your purchase order first, then do follow-on work — pushing stock to Shopify, updating incoming quantities. Once the order is saved that work can’t be undone, so a failure in it is reported, not thrown:
{ "data": { "purchaseOrder": { "purchaseOrderId": "po_7c3e", "status": "Received" }, "status": { "before": "In Progress", "after": "Received" }, "sideEffects": { "shopifySync": { "status": "failed", "detail": "Shopify returned 429 for 2 items" } }, "recovery": "Reset the sync bookkeeping and re-sync: POST /shopify-sync/reset then POST /shopify-sync" }, "meta": { "requestId": "c5a0…", "apiVersion": "1.1" }}Handling retries
Section titled “Handling retries”- On
429/503, waitRetry-Afterseconds (orretryAfterMs) and retry the same request. - On
5xxduring a write, you often can’t tell whether it went through. Send anIdempotency-Keyand retrying is always safe — a stored success is replayed instead of re-run, and a5xxis never stored, so a genuine retry really re-executes. - Don’t fan out parallel requests against one shop — the data store is single-threaded per shop, so concurrency doesn’t go faster, can trip the rate limit, and on writes can collide with an in-flight stock sync.