Errors
Every failure uses the same envelope as success, with data: null:
{
"statusCode": 404,
"errorCode": "not_found",
"message": "Resource not found",
"data": null
}
Branch on errorCode — it is a stable contract. message can be reworded at any time and is meant for logs and humans, not for if statements.
Error codes
| errorCode | HTTP status | When |
| --- | --- | --- |
| bad_request | 400 | The request is malformed: a missing required field, a bad value, an out-of-range limit, and similar. |
| unauthorized | 401 | No access token, an expired one, or the API key behind it is disabled, deleted, or replaced. See Authentication. |
| forbidden | 403 | The token is valid, but the key's permission (or the caller's role on that specific resource) doesn't allow this action. |
| not_found | 404 | The resource doesn't exist, or belongs to another organization. These are deliberately indistinguishable — a caller can never use this API to probe for the existence of something it doesn't own. |
| conflict | 409 | The request collides with an in-progress operation — currently only idempotency_in_progress (see below). |
| unprocessable_entity | 422 | The request is well-formed but cannot be applied — currently only idempotency_key_reuse (see below). |
| invalid_cursor | 400 | The cursor query parameter isn't a value this API issued. See Pagination. |
| rate_limited | 429 | The organization has used up its request allowance for the current window. See below. |
| payload_too_large | 413 | The request body is over 1 MB. |
| internal_error | 500 | Something failed on our side. message never contains a stack trace or exception text — quote the X-Request-Id header if you contact support. |
Rate limits
{
"statusCode": 429,
"errorCode": "rate_limited",
"message": "...",
"data": null
}
with a Retry-After header (seconds) telling you how long to wait. Limits apply per organization, per billing cycle, and cover reads and writes together — using up the allowance with reads blocks writes too, and vice versa. Back off and retry after the Retry-After interval rather than immediately; retrying immediately just spends another request hitting the same limit.
Retrying safely
A POST that creates something is not safe to blindly retry on a network timeout — you might create the resource twice. Send an Idempotency-Key header on every creating POST, and a retry with the same key is guaranteed to return the original result rather than a duplicate. See Idempotent writes for the exact rules.
GET, PATCH and DELETE are generally safe to retry as-is: GET has no side effects, and a PATCH/DELETE that repeats after a previous attempt actually succeeded either applies the same change again harmlessly or returns 404 not_found for a resource that's already gone — neither is an error worth treating specially.