REST API
Errors
The error envelope, stable codes, and how to retry safely.
Every non-2xx response carries the same flat JSON envelope: a stable machine-readable error code, a human-readable message, the HTTP status, and a _tag naming the specific failure:
{
"_tag": "ApiUnauthorized",
"error": "unauthorized",
"message": "Invalid API key.",
"status": 401
}Write code against error (or status), never against message — messages can be reworded at any time. _tag is more specific than error and is stable too, but new tags appear as the API grows.
Common codes
| HTTP | error | Meaning |
|---|---|---|
| 400 | bad_request | The request was well-formed JSON but failed validation — bad query param, unknown field key, wrong value shape. |
| 401 | unauthorized | Missing, malformed, expired, or revoked API key — or a session where a key is required. |
| 403 | forbidden | The key is valid but lacks a required scope or item grant. |
| 404 | not_found | The resource doesn't exist — or the key can't see it. |
| 409 | conflict | The write collided with current state (e.g. a taken slug or replayed idempotency key with a different body). |
| 410 | gone | The resource existed but is permanently gone. |
| 413 | payload_too_large | Upload or body exceeded its size limit. |
| 429 | rate_limited | Too many requests — see Rate limits. |
| 500 | persistence_failed / internal_server_error | Something failed on our side. |
not_found deliberately covers both "doesn't exist" and "not visible to this key", so probing for ids reveals nothing.
Retrying
rate_limitedand 5xx are retryable. Back off exponentially and honorRetry-Afterwhen present.- Mutations should always send an
Idempotency-Keyheader, so a retried request that already succeeded returns the original result instead of duplicating work. conflictmeans re-read the resource, reapply your change, and try again — retrying the identical request will keep failing.- Everything else (
unauthorized,forbidden,bad_request) is a bug in the request; retrying without changes won't help.