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…"
}
}
code— the machine-readable reason. This is the field to branch on.message— a human-readable explanation. It can change at any time; never parse or branch on it.request_id— correlation id (also in theX-Request-Idheader). Quote it when contacting support.
Published error codes
The code set only ever grows. Treat an unknown code as a generic failure of its HTTP status.
| Code | Status | Meaning |
|---|---|---|
unauthorized | 401 | No credential, or one that no longer resolves (e.g. a revoked key). |
forbidden | 403 | The credential has no authority here — a browser/Shopify session on the public API, or an API key outside /api/v1. |
forbidden_scope | 403 | The key is valid but lacks the route's required scope. |
not_found | 404 | No 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_failed | 400 | A parameter was understood but rejected — a malformed updated_since, a foreign cursor, an unknown status. |
rate_limited | 429 | Over the per-key or per-account minute budget. Wait Retry-After seconds. |
pim_locked | 423 | Writes are held while an import or store refresh is being reconciled. Not reachable on the current read endpoints; published for forward compatibility. |
conflict | 409 | A stale draft_revision on a write. Not reachable on the current read endpoints; published for forward compatibility. |
internal_error | 5xx | Something failed on our side. Safe to retry with backoff; report persistent ones with the request_id. |
Rate limits
Two per-minute budgets apply simultaneously:
- 120 requests/minute per key
- 240 requests/minute per account — so many keys can't multiply the account's budget.
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
- On
429: waitRetry-Afterseconds, then retry. Don't hammer — the window is a fixed minute. - On
5xx: retry with exponential backoff and jitter; give up after a few attempts and surface therequest_id. - On
4xxother than 429: don't retry unchanged — the request itself is the problem. - Spread bulk work: with
limit=250, a full catalog walk rarely needs more than a few requests per minute. - Use ETags and
updated_sinceso recurring syncs consume almost no budget.
Versioning & deprecation
- Within v1, changes are additive only — ignore fields you don't recognize.
- Breaking changes would ship as
/api/v2, announced withDeprecationandSunsetheaders on v1 and in the changelog.