Errors
Every refusal answers with the same envelope: a stable code to branch on, and a message written for a person, which may be reworded. The code is repeated in the x-error-code header, and every answer carries a request-id: log it beside your own errors, and quote it when asking for help.
{
"error": {
"code": "refused",
"message": "An inactive or sold-out prepared item cannot be put on sale."
}
}Codes
All 20. Each endpoint’s page lists the ones it can answer, with what they mean there.
invalid_idempotency_keyA–Z a–z 0–9 - _ . : ~ + / =.What to do: Send a UUID.
idempotency_key_requiredWhat to do: Generate one per operation; reuse it only to retry that operation.
missing_api_keyAuthorization: Bearer header and no x-api-key header.What to do: Send the key as Authorization: Bearer <key>.
invalid_api_keyWhat to do: Check the key on API Keys; make a new one if it was revoked or expired.
insufficient_scopeWhat to do: Scopes are fixed when a key is made: make a new key with the scope.
issuer_access_endedWhat to do: Make a new key from an active account.
issuer_permission_changedWhat to do: Ask an owner to restore the permission, or make a new key from an account that holds it.
permission_requiredWhat to do: Use a key with the named scope, from a creator who holds the named permission.
not_foundWhat to do: Check the id with the matching list endpoint.
unknown_endpointWhat to do: Check the path against /docs.
method_not_allowedAllow header lists what does.What to do: Use a method from Allow.
refusedWhat to do: Read the sentence: it says what to change. The endpoint page lists the common ones.
idempotency_key_in_useWhat to do: Wait Retry-After seconds and send the same request again.
payload_too_largeWhat to do: Send less; split a batch.
json_requiredContent-Type: application/json.What to do: Send JSON with that header.
invalid_requestWhat to do: Fix the named fields. Nothing was done, and an Idempotency-Key is not used up.
invalid_cursorWhat to do: Start without cursor.
idempotency_key_reusedWhat to do: Use a new key for a new request.
rate_limitedWhat to do: Wait Retry-After seconds. A bad key will not start working by waiting.
internal_errorWhat to do: Retry with the same Idempotency-Key; quote the request-id if it persists.
- Possible on every endpoint, so each page prints them once, in a box:
missing_api_key,invalid_api_key,rate_limited,internal_error. - Never an endpoint’s own answer, since they mean the path or the method is wrong:
unknown_endpointandmethod_not_allowed. A405lists the methods the path takes inAllow;OPTIONSon a path answers the same list with no key.
When the restaurant says no
409 refused is the restaurant’s own rule speaking: an item that is sold out, a table with an order already open, an order that has taken money. Its message is the sentence the console shows for the same act, except that it never names a guest or a member of staff. Show it to a person; fix what it says; do not retry it unchanged. Each endpoint’s page lists its common refusals.
Retrying
429 rate_limitedand409 idempotency_key_in_use: waitRetry-Afterseconds, then send the same request with the same Idempotency-Key.500 internal_error, a timeout or a dropped connection: send the same request with the same Idempotency-Key. If the first one landed, you get its answer back and nothing runs twice.- Any other 4xx: nothing about it changes on a second try. Fix what the message names first; after a
refused, send the corrected request with a new key.
Kept and released under an Idempotency-Key
A write’s answer is kept against its Idempotency-Key, so a retry gets the same answer: a success, 404 not_found and 409 refused. 422 invalid_request and a 5xx are released: correct the body, or retry, with the same key. A request refused before its key is read (a 400, 401, 403, 413, 415 or 429) does not use the key up. See Idempotency.
Reading an error
Branch on error.code, never on the message. Check the content type before parsing: a proxy or a load balancer can answer before the API does, and its page is not JSON.
const response = await fetch(url, init);
// A proxy's error page is not JSON: read the body as JSON only when it says so.
const isJson = response.headers.get("content-type")?.includes("application/json");
const body = isJson ? await response.json() : null;
if (!response.ok) {
const code = body?.error?.code;
if (code === "rate_limited" || code === "idempotency_key_in_use") {
// Wait Retry-After seconds, then send the same request, same Idempotency-Key.
} else if (code === "refused") {
// The restaurant's own rule: show body.error.message to a person.
}
throw new Error(body?.error ? `${code}: ${body.error.message}` : `HTTP ${response.status}`);
}response = requests.post(url, headers=headers, json=payload, timeout=30)
# Not raise_for_status(): it drops error.message, the sentence that says what to fix.
body = response.json() if response.headers.get("content-type", "").startswith("application/json") else None
if not response.ok:
code = body["error"]["code"] if body and "error" in body else None
if code in ("rate_limited", "idempotency_key_in_use"):
... # wait Retry-After seconds, then send the same request, same Idempotency-Key
elif code == "refused":
... # the restaurant's own rule: show body["error"]["message"] to a person
raise SystemExit(f"{code}: {body['error']['message']}" if code else f"HTTP {response.status_code}")rate_limited is either the key’s own window or, before a key is known, too many unknown keys from one network. Both clear by waiting Retry-After seconds; a key that is not valid will not start working by waiting.