Skip to content

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 envelope409
{
  "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.

400invalid_idempotency_key
The Idempotency-Key header is not 8–255 of A–Z a–z 0–9 - _ . : ~ + / =.

What to do: Send a UUID.

400idempotency_key_required
A POST was sent without an Idempotency-Key.

What to do: Generate one per operation; reuse it only to retry that operation.

401missing_api_key
No Authorization: Bearer header and no x-api-key header.

What to do: Send the key as Authorization: Bearer <key>.

401invalid_api_key
The key is unknown, revoked or expired (answered alike).

What to do: Check the key on API Keys; make a new one if it was revoked or expired.

403insufficient_scope
The key does not carry the endpoint's scope.

What to do: Scopes are fixed when a key is made: make a new key with the scope.

403issuer_access_ended
The key's creator has left the restaurant, was suspended, or was removed and invited back.

What to do: Make a new key from an active account.

403issuer_permission_changed
The key's creator no longer holds any permission that could put this scope on a key.

What to do: Ask an owner to restore the permission, or make a new key from an account that holds it.

403permission_required
The key's permissions for this endpoint (its creator's, cut down to the endpoint's scope) do not include what this endpoint, or this argument, needs. The message names it.

What to do: Use a key with the named scope, from a creator who holds the named permission.

404not_found
No such record in this restaurant. Another restaurant's id answers the same.

What to do: Check the id with the matching list endpoint.

404unknown_endpoint
No endpoint has this path.

What to do: Check the path against /docs.

405method_not_allowed
The path exists; the method does not. The Allow header lists what does.

What to do: Use a method from Allow.

409refused
The engine refused the act; the message is its own sentence, the same the console shows, except that it never names a guest or a staff member.

What to do: Read the sentence: it says what to change. The endpoint page lists the common ones.

409idempotency_key_in_use
Another request with this key is still running.

What to do: Wait Retry-After seconds and send the same request again.

413payload_too_large
The body is over 1 MiB.

What to do: Send less; split a batch.

415json_required
A write without Content-Type: application/json.

What to do: Send JSON with that header.

422invalid_request
The request does not match the endpoint's published schema: a missing or unknown field or query parameter, a value out of range. The message names the first four problems.

What to do: Fix the named fields. Nothing was done, and an Idempotency-Key is not used up.

422invalid_cursor
The cursor is not in this list with these filters.

What to do: Start without cursor.

422idempotency_key_reused
The key was used for a different request in the last 24 hours.

What to do: Use a new key for a new request.

429rate_limited
Over the key's 60 units a minute, or over 30 unknown keys a minute from one network (an address; an IPv6 caller's /64).

What to do: Wait Retry-After seconds. A bad key will not start working by waiting.

500internal_error
Something failed on our side. Nothing kept the Idempotency-Key.

What 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_endpoint and method_not_allowed. A 405 lists the methods the path takes in Allow; OPTIONS on 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_limited and 409 idempotency_key_in_use: wait Retry-After seconds, 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.

JavaScript
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}`);
}
Python
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}")
Two different 429s, one 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.