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…"
  }
}

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.

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 — a read-only key on a pim:write endpoint, or any key at all on the reserved pim:delete endpoint.
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 or body was understood but rejected — a malformed updated_since, a foreign cursor, an unknown status or scan dimension.
rate_limited429Over the per-key or per-account minute budget. Wait Retry-After seconds — budgets and headers on Rate limits.
pim_locked423Writes 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.
conflict409A 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_required409A 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_conflict409A running background publish owns records needed by the write. Retry after that operation finishes; this also protects the public data-health write routes.
internal_error5xxSomething 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.

Rate-limit budgets moved.

The per-key and per-account budgets, the X-RateLimit-* headers, and the full retry guidance now live on Rate limits.