Errors
Every refusal is a problem document: an HTTP status that carries the meaning, and a body that explains the occurrence. Branch on the status.
The body
Errors come back as application/problem+json, the format described by RFC 7807. Three fields are always present.
{
"title": "Unauthorized",
"status": 401,
"detail": "invalid or expired API key"
}The HTTP status, repeated in the body. This is the field to branch on.
Short summary of the kind of problem, stable across occurrences of the same status.
A sentence about this particular occurrence, meant for a developer reading a log. Do not match on it: the wording can change without the status changing.
The contract declares these three as well. When a request fails validation, errors names each offending field, carrying its location in the request, a message and the value that was rejected.
A validation failure is the one worth handling specially, because it tells you exactly what to change. Here a calendar window was sent as bare dates instead of timestamps:
{
"title": "Unprocessable Entity",
"status": 422,
"detail": "validation failed",
"errors": [
{
"message": "invalid date/time for format 2006-01-02T15:04:05.999999999Z07:00",
"location": "query.from",
"value": "2026-07-27"
}
]
}There is no machine-readable code beside the status, so the status is the only thing safe to branch on. Do not match on title or detail.
Statuses
Any endpoint on the API can answer with any of these.
No bearer token, or a token that is unknown, revoked or expired. All four answer identically on purpose, so a caller probing tokens learns nothing about which is which.
The organization’s plan does not include the public API. It is available on Pro and above, and the check runs on every request, so a downgrade stops keys that were minted earlier.
The organization is not active.
The key is over its rate limit. Back off and retry.
Something failed on our side. The body carries a short sentence and never an internal error string, so quote your own request timestamp when reporting one.
What to retry
- 429 is the one worth retrying automatically. Wait for the window to reset rather than retrying immediately.
- 500 may be transient, so one retry with a delay is reasonable. Repeated 500s are ours to fix, not yours to work around.
- 401, 402 and 403 will not resolve by retrying. Something has to change first: the credential, the plan or the organization.
There are no idempotency keys, so retrying a POST that actually succeeded can create a second record. Retry writes deliberately rather than automatically, and check before you repeat one.
A path that does not exist under /v1 answers 404 as plain text rather than JSON. Treat a non-JSON error body as a wrong URL.