Errors
When a request fails, the body is an error envelope:
{ "error": { "code": "NOT_FOUND", "message": "Not found: VMAT-VAR-7f3d2c1a-9b4e-4c8d-a2f6-5e1b0d9c8a7f", "retryable": false, "requestId": "0f8c…" }}retryable— whether retrying the same request can succeed. Retryable errors also includeretryAfterMsand aRetry-Afterresponse header (in seconds).action— sometimes present: a human-readable next step (e.g. onPLAN_REQUIRED).requestId— also in theX-Request-Idheader. Quote it in support requests.
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 Enhanced plan (the REST API is Enhanced-only) | — |
FORBIDDEN_SCOPE | 403 | A PATCH with a read-only key | — |
INVALID_INPUT | 400 | A bad query parameter or body (the message names the problem), or a write against a Shopify-managed material (raw-materials endpoints) | — |
NOT_FOUND | 404 | Unknown path, or the id doesn’t exist | — |
CONFLICT | 409 | The request conflicts with the current state — a stale expectedUpdatedAt on a work-order write, a transition that isn’t allowed, or a build run that can’t start | — |
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 (e.g. the live inventory read) | 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 | — |
An unsupported method returns 405 with code METHOD_NOT_ALLOWED and an Allow header — the only code outside the table above.
Virtual-only material writes
Section titled “Virtual-only material writes”On the two /raw-materials/* write endpoints, a PATCH whose path id is not a VMAT-* id fails fast with 400 INVALID_INPUT:
{ "error": { "code": "INVALID_INPUT", "message": "Shopify-managed materials are read-only in API v1 — only virtual (VMAT-*) materials can be modified (got '45067340286136')", "retryable": false, "requestId": "…" }}This rule is about materials. The three /work-orders/* writes take a work-order id and are not subject to it — see Work-order conflicts below.
Work-order conflicts
Section titled “Work-order conflicts”The three work-order writes answer 409 CONFLICT rather than overwriting or half-applying:
- A stale
expectedUpdatedAt— someone changed the work order after you read it. Re-read it and re-send. - A transition that isn’t allowed from the work order’s current state; the response names the blockers.
- A build run that can’t start — the response carries the specific reason: Shopify holds no inventory record for a line at that location, Shopify refused the push, the run would over-commit, or the run is empty. On any of them nothing was picked and nothing was pushed.
A 409 is never retryable as-is: fix the cause, or re-read and re-send with a current token.
Unit-change conflicts
Section titled “Unit-change conflicts”Changing the unit of a material that is already used in BOMs or assembly bills is only allowed when a usable unit conversion exists. When it doesn’t, the app’s conflict response is passed through as 400 INVALID_INPUT — the message carries the app’s explanation of the conflict (which units were involved and why no conversion applies), so you can surface it directly to the user.
Handling retries
Section titled “Handling retries”On 429 / 503, wait Retry-After seconds (or retryAfterMs) and retry the same request. Don’t fan out parallel requests against one shop — the data store is single-threaded per shop, so concurrency doesn’t go faster and can trip the rate limit.