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 | — |
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 (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 writes
Section titled “Virtual-only writes”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": "…" }}Unit-change conflicts
Section titled “Unit-change conflicts”Changing the unit of a material that is already used in BOMs or sub-assemblies 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.