Errors & Responses
Success envelopes
Successful reads and writes return the action's data payload directly, with no wrapper. The HTTP status code is the contract:
| Code | When |
|---|---|
200 OK | Read completed |
202 Accepted | Write accepted — this is the success code for all writes. Synchronous writes return the updated resource snapshot; bulk/async actions return {job_id, status_url} — see Bulk & Async Jobs |
204 No Content | Action succeeded with no payload (rare) |
207 Multi-Status | Reserved for bulk actions reporting per-item failures |
v2 writes return 202 Accepted even when the local database write committed synchronously — Shopify-side sync (metafields, selling-plan groups, order mutations) generally completes in the background. The response body reflects the committed local state.
Error envelope
All non-2xx responses share one shape:
{ "errors": ["one or more human-readable strings"] }errors is always an array of strings. Even single-error responses come back as a one-element array.
Status codes
| Code | Meaning | Typical cause |
|---|---|---|
400 Bad Request | Malformed JSON | Missing/invalid JSON body |
401 Unauthorized | Missing or invalid X-Auth-Token | Wrong key or no header |
404 Not Found | Unknown action path, or resource not found | Typo in path, deleted offer, expired job ID |
409 Conflict | State-machine refused the transition | e.g., release on a cancelled order, set_collection on a non-collection offer |
422 Unprocessable Entity | Schema validation failure | Missing required field, bad enum value, over a bulk cap |
429 Too Many Requests | Rate limit exhausted | See Rate Limits |
500 Internal Server Error | Server bug — please report | Unexpected exception |
404 — unknown path vs missing resource
An unregistered verb+path combination:
{ "errors": ["No v2 action registered for POST /preorders/offers/nope"] }A registered path addressing a resource that doesn't exist in this shop:
{ "errors": ["Preorder offer not found: 5b2e..."] }If you hit the first kind, ask the API itself: GET /help?prefix=... or GET <scope>/help returns the registered actions under any scope — see Dispatch & Discovery.
409 — state-machine conflict
State-mutating actions check whether the transition is legal before running. If not, they return 409 with a one-line reason:
{ "errors": ["Cannot release: order is already cancelled."] }Some 409s can be overridden with force: true on the request body (e.g., releasing an order before the balance is paid). The action's notes in the /help manifest will say so.
422 — validation
Schema breaches surface as one error per field:
{ "errors": ["mode must be one of: auto, manual"] }Caps are validation errors too:
{ "errors": ["Too many order_ids: 1200 (max 1000 per request)"] }Idempotency
v2 does not support request-level idempotency keys (yet). Practical rules:
- Reads are safe to retry.
- Lifecycle actions are idempotent in their effect (enabling an already-enabled offer is a no-op success).
- Bulk submissions create a new job each time — don't auto-retry without checking the previous
job_idfirst. - Creates are not idempotent — duplicate submissions create duplicate resources.
Stable error messages
We treat the human-readable strings in errors as part of the API contract for cross-cutting messages — "X is required", "Y must be one of: ...". Resource-specific 409 reasons may evolve over time; don't pattern-match on those.
