Errors & limits

One error envelope, a small published code set, per-minute rate limits with self-describing headers. Branch on code, never on message.

The error envelope

Every failure, on every endpoint, is:

{
  "error": {
    "code": "not_found",
    "message": "no such product in this account",
    "request_id": "req_8f3a…"
  }
}

Published error codes

The code set only ever grows. Treat an unknown code as a generic failure of its HTTP status.

CodeStatusMeaning
unauthorized401No credential, or one that no longer resolves (e.g. a revoked key).
forbidden403The credential has no authority here — a browser/Shopify session on the public API, or an API key outside /api/v1.
forbidden_scope403The key is valid but lacks the route's required scope.
not_found404No such record in this account. Another account's record, an id of a different entity kind, and an entity with no version on the requested store all answer identically.
validation_failed400A parameter was understood but rejected — a malformed updated_since, a foreign cursor, an unknown status.
rate_limited429Over the per-key or per-account minute budget. Wait Retry-After seconds.
pim_locked423Writes are held while an import or store refresh is being reconciled. Not reachable on the current read endpoints; published for forward compatibility.
conflict409A stale draft_revision on a write. Not reachable on the current read endpoints; published for forward compatibility.
internal_error5xxSomething failed on our side. Safe to retry with backoff; report persistent ones with the request_id.

Rate limits

Two per-minute budgets apply simultaneously:

Every metered response carries the counters, so a client can pace itself without guessing:

X-RateLimit-Limit: 120
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1753970460

Over budget answers 429 rate_limited with a Retry-After header (seconds until the window rolls over).

Recommended retry behavior

Versioning & deprecation