Errors and rate limits
The one error envelope every failed request returns, the rate-limit headers, and how to retry a write safely.
Every endpoint fails the same way, so a client handles errors, rate limits and retries once and reuses that code everywhere, including over MCP.
Every non-2xx response is one JSON object:
{
"defined": false,
"code": "TOO_MANY_REQUESTS",
"message": "Too many requests",
"data": { "reason": "rate_limit.exceeded", "retryAfterMs": 12000 }
}codeis the HTTP-level error, for exampleUNAUTHORIZED,FORBIDDEN,NOT_FOUND,BAD_REQUESTorTOO_MANY_REQUESTS.data.reason, when present, is a finer machine-readable reason such asauth.key_not_foundorrate_limit.exceeded. Branch on it rather than onmessage, which is for people and may change.definedistruewhen the operation lists that error in the API reference, andfalsefor errors any request can meet (authentication, rate limits, an unknown route).- A validation failure answers
BAD_REQUESTwith the problems indata.formErrorsanddata.fieldErrors.
Every response also carries an X-Request-ID. Quote it when you contact
support, and we can find that exact request.
| Status | Code | What to do |
|---|---|---|
400 | BAD_REQUEST | Fix the request; data.fieldErrors names the fields |
401 | UNAUTHORIZED | Send a valid key or session |
403 | FORBIDDEN | The key lacks the scope or the role the operation needs |
404 | NOT_FOUND | The resource does not exist, or you are not allowed to see it |
409 | CONFLICT | A duplicate write is still in flight; retry shortly |
413 | PAYLOAD_TOO_LARGE | The request body is over 1 MB; split a bulk push into smaller batches |
422 | UNPROCESSABLE_CONTENT | The request is well formed but cannot be applied |
429 | TOO_MANY_REQUESTS | Wait for Retry-After, then retry |
Every response states the limit it was counted against, in two forms:
- the
X-RateLimit-*headers; - the IETF structured fields
RateLimit(live state:ris the requests remaining,tthe seconds until the window resets) andRateLimit-Policy(the quota:qis the limit,wthe window in seconds).
A 429 also carries Retry-After in seconds and data.retryAfterMs. Wait at
least that long before the next request; retrying sooner is counted and
refused again.
A write operation that lists an Idempotency-Key header in the
API reference can be retried without doing the work twice. Send
one key per logical write and repeat the same key on every retry:
- the same key with the same body within 24 hours replays the stored response;
- the same key with a different body is refused with
422; - a duplicate that arrives while the first is still running gets
409.
An operation without the header is not idempotent, so retry it only when you know the first attempt did not land.