Skip to content

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.

codeHTTPMeaningRetry-After
UNAUTHORIZED401Missing, invalid, or revoked token
PLAN_REQUIRED403The shop isn’t on the Elevate plan (the REST API is Elevate-only)
FORBIDDEN_SCOPE403The token’s scope can’t do this — e.g. a read key on a write endpoint
INVALID_INPUT400A bad parameter, a bad body, or a change the order’s current state doesn’t allow (the message names it)
NOT_FOUND404Unknown path, or the id doesn’t exist
RATE_LIMITED429Per-shop rate limit reached
DO_OVERLOADED503The shop’s data store is busy
DO_UNAVAILABLE503The data store is temporarily unavailable
UPSTREAM_SHOPIFY502A Shopify-facing dependency failedif present
TIMEOUT504The request timed outif present
OUTPUT_TOO_LARGE413The response exceeded the size cap
INTERNAL500An 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 — use POST …/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 locationId first with PATCH …/{id}.
  • /complete on an order that isn’t Received or Partially Received — receive first, or use POST …/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 a resolutions array saying what to do with each one. See the lifecycle page.

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 a 502 leaves 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 with POST …/shopify-sync/reset followed by POST …/shopify-sync, or simply retry /quick-complete.

Either way, re-read the order with GET /api/v1/purchase-orders/{id} before deciding.

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" }
}
  • On 429 / 503, wait Retry-After seconds (or retryAfterMs) and retry the same request.
  • On 5xx during a write, you often can’t tell whether it went through. Send an Idempotency-Key and retrying is always safe — a stored success is replayed instead of re-run, and a 5xx is 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.