Errors, Pagination & Idempotency
The conventions every Partner API v1 endpoint follows.
Error envelope
Every error response is JSON with a matching HTTP status:
{ "error": "This installation was not granted the \"write_products\" scope" }Some routes add extra fields on top:
{ "error": "Manifest validation failed", "issues": ["version: version must be semver (x.y.z)"] }
{ "error": "Review scan rejected this submission", "issues": [...], "warnings": [...] }Standard status codes
| Status | Meaning |
|---|---|
| 400 | Malformed request (bad JSON, invalid id, missing required field) |
| 401 | Missing/invalid/expired bearer token |
| 403 | Missing scope/capability, or installation not active |
| 404 | Resource not found (or store not found — tenancy is derived from your token, never the request) |
| 409 | Idempotency-Key reused with a different request body |
| 422 | Semantically invalid (e.g. a plan limit reached, an Extension Storage quota exceeded) |
| 429 | Rate limit exceeded |
Pagination
Every list endpoint uses cursor pagination:
GET /platform/v1/products?cursor=<lastId>&limit=<n>limit— default 50, max 200.- Response shape:
{ "<resource>": [...], "nextCursor": "<id>" | null }. - The cursor is simply the last row's own id (ascending sort) — not an opaque token. There's no promise of cursor opacity as part of the contract.
let cursor: string | undefined;
do {
const { products, nextCursor } = await client.listProducts(cursor, 200);
// ...process products
cursor = nextCursor ?? undefined;
} while (cursor);Idempotency
Every write (POST/PATCH/DELETE) requires an Idempotency-Key header.
await client.createProduct({ name: "Mug", price: 499 }, crypto.randomUUID());curl -X POST .../products -H "Idempotency-Key: 6b1f2b1a-..." -d '{...}'Behavior:
- Fresh key → runs the handler, stores the result.
- Repeated key, same request body → replays the stored response, doesn't re-run the handler. Safe to retry a dropped connection.
- Repeated key, different request body → rejected (
400; the header being absent entirely is a separatemissing_keyerror). A body mismatch on an existing key is a client bug, not a legitimate retry. - Records expire after 24 hours.
Generate one key per logical operation, not one per call
The whole point of the header is that a retry of the same operation reuses the same key. If you generate a fresh UUID on every call (including retries), you get zero idempotency protection — you've just added an unused header. Generate the key once per logical write, and reuse it across your own retry attempts of that exact write.
Rate limiting
A flat 100 requests / 60-second rolling window per installation. Exceeding
it returns 429. There is no X-RateLimit-* response header today —
build your own backoff-on-429 logic rather than trying to pre-empt the
limit from a quota header that doesn't exist.