Using the API
Errors
One error envelope and a small published code set, on every endpoint. 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 — the versioning promise guarantees no existing code changes meaning.
| 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 — a read-only key on a pim:write endpoint, or any key at all on the reserved pim:delete endpoint. |
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 or body was understood but rejected — a malformed updated_since, a foreign cursor, an unknown status or scan dimension. |
rate_limited | 429 | Over the per-key or per-account minute budget. Wait Retry-After seconds — budgets and headers on Rate limits. |
pim_locked | 423 | Writes are held while an import or store refresh is being reconciled. Reachable since the data-health write endpoints shipped — retry after the merchant finishes the review. |
conflict | 409 | A stale revision on a write, or another write conflict. Catalog canonical revisions are integers; product/variant store revisions are opaque strings. A stale write returns current_revision and applies nothing. Read again before reapplying the edit. |
parent_publish_confirmation_required | 409 | A variant publish would also publish its parent product and eligible sibling variants. Inspect the returned scope, then resend with parent_publish_confirmation.fingerprint only after approval. |
publish_lock_conflict | 409 | A running background publish owns records needed by the write. Retry after that operation finishes; this also protects the public data-health write routes. |
internal_error | 5xx | Something failed on our side. Safe to retry with backoff; report persistent ones with the request_id. |
Write error details
Catalog and discount writes may include error.current_revision on stale-write conflicts. Catalog validation errors can name one field in error.field (for example mf:custom.material); discount validation uses error.field_errors. A rejected catalog create or PATCH applies no partial draft changes.
Parent-publication challenges carry a fingerprint and product/store scope in the error detail. The confirmation must match the latest scope; an obsolete fingerprint asks for confirmation again. The API never treats a timeout as consent.
When Shopify is the failure: 502
The sales-channel reads query Shopify live
rather than Peak PIM's own rows. They are the only endpoints that can answer
502 — the store could not be reached — and they carry Shopify's
latency rather than ours. A 502 uses the same envelope with internal_error; treat it
like a 5xx and retry with backoff.
Correlate with X-Request-Id
Every response — success or failure — carries an X-Request-Id header,
and every error body repeats it as request_id. Log it alongside your
own request logging, and quote it when reporting a problem: it lets support find
the exact request on our side.
The per-key and per-account budgets, the X-RateLimit-* headers, and the full retry guidance now live on Rate limits.