Concepts
Errors that can be told apart
Every refusal shares one shape, so you write your handling once: a stable machine-readable
error code, and a
message in prose.
The contract
Branch on error, never on message: the code never changes for a
given condition, while the sentence may be reworded between releases. Some refusals carry
one extra field named in the table below, such as required_ability on a 403
or the per-field errors bag on validation.
The four refusals worth telling apart are the ones whose next step differs: 401 rotate the credential, 402 change the plan (the message names the tier that lifts it), 403 grant the ability, 404 the id is not this team's. A plan limit never arrives disguised as a permissions error.
{
"error": "missing_ability",
"message": "This token does not have the reports:export ability.",
"required_ability": "reports:export"
}
{
"error": "validation_failed",
"message": "The request could not be accepted as sent.",
"errors": {
"window_start": [
"The period has to start in the past. The weather has not happened yet."
]
}
}
Every status, every code
| Status | Code | Meaning | What to do |
|---|---|---|---|
| 401 | unauthenticated |
The credential is missing, unknown, expired or revoked. | Check the Authorization header and rotate the token if it has been revoked. |
| 402 | plan_limit |
The plan does not include this, or its allowance is spent. | The message names the tier that would lift it. Change the plan, or wait for the allowance to renew. |
| 403 | missing_ability |
The credential is valid but was not granted the ability this endpoint requires. | Grant the ability named in required_ability, or use a token that holds it. |
| 404 | not_found |
No such record for this team. Another team's id answers the same way, deliberately. | Check the id. A 404 never confirms whether the id exists elsewhere. |
| 405 | method_not_allowed |
The path exists but not with this verb. | Check the method against the reference. |
| 409 | conflict |
The request is at odds with current state: a duplicate address, a reused Idempotency-Key, a document not yet generated. |
The message says which. Each 409 in the reference states what to do. |
| 422 | validation_failed |
A field failed validation. errors names each field with a sentence. |
Fix the named fields and resend. |
| 422 | invalid |
The request was well formed but cannot be acted on: unreadable GeoJSON, an address that cannot be located. | The message is the explanation. Fix the input rather than retrying. |
| 429 | rate_limited |
Too many requests in the window. | Wait the interval in the Retry-After header, then resume. |
| 500 | server_error |
Something failed on our side. The response never carries internals. | Retry, and contact support if it persists. |
| 503 | unavailable |
A dependency of the endpoint is down, such as the PDF renderer. | An outage of ours, not a billing problem. Retry later. |
What a 500 never carries
A failure on our side is reported on our side. The response says so in the same shape as every other refusal and never carries internals: no stack, no query, nothing that belongs in our logs rather than yours.