Errors
A failed request answers a non-2xx status with a JSON body. Route handlers answer the flat envelope:
{ "status": 422, "code": "VALIDATION_ERROR", "message": "Invalid document payload", "fields": { "rootAtoms": "Not bound to this document type: 1234…" } }The token authenticator and the two raw design-token exports (.css, .tokens.json) answer the same fields nested under error:
{ "error": { "status": 401, "code": "INVALID_API_TOKEN", "message": "Invalid, expired, or revoked API token" } }Branch on code; message is localized prose. Some codes carry extra fields, noted below. A 429 also sets a Retry-After header (seconds).
Codes
| Code | Status | Meaning | What to do |
|---|---|---|---|
API_TOKEN_REQUIRED | 401 | No Authorization: Bearer atk_… or X-API-Key header | Send the token |
INVALID_API_TOKEN | 401 | The token is unknown, revoked or expired, or belongs to another organization than the URL's | Regenerate or create a token; check org_id |
TOKEN_ORG_MISMATCH | 403 | The token does not belong to the URL's organization | Use the right org_id |
TOKEN_PROJECT_MISMATCH | 403 | The token is narrowed to one project and the URL names another | Use the token's project, or an organization-wide token |
TOKEN_SCOPE_READ_ONLY | 403 | A write with a read token; carries requiredPlan | Create a read + write token |
FEATURE_DISABLED | 403 | The organization's plan has no API writes; carries feature and requiredPlan | Upgrade in Studio → Settings → Billing |
ACCOUNT_CLOSED | 403 | The organization has been closed | Do not retry |
DELIVERY_BLOCKED | 403 | Content delivery for the organization is blocked by the platform | Do not retry; the organization's owner can contact support from Studio |
BILLING_TERMINATED | 402 | The subscription was terminated for non-payment | An owner settles the invoice in Studio → Settings → Billing |
NOT_FOUND | 404 | Unknown org id, project slug or resource — or a resource that is not published, to a caller without draft access | Check the slug; use ?status=draft with a read_write token to see drafts |
VALIDATION_ERROR | 422 | The write payload was refused; fields names each problem (an unknown atom uuid, a missing title, a bad parent term, a refused upload URL) | Fix the named fields; nothing was written |
SLUG_CONFLICT | 409 | The requested slug is taken within the type | Omit slug to derive a unique one, or choose another |
VERSION_LOCKED | 409 | An editor has the document or term open; holder.name and expiresAt say who and until when; nothing was written | Retry after the lock lapses |
LIMIT_EXCEEDED | 403 | An upload would exceed the plan's storage cap | Free space in Studio → Media, or upgrade |
UNSUPPORTED_MEDIA_TYPE | 415 | The upload is not an accepted image type | Send a JPEG, PNG, GIF, WebP, AVIF or SVG |
EMPTY_FILE · UPLOAD_FAILED | 400 | The upload carried no usable file; errors lists per-file failures | Check the multipart body |
RATE_LIMITED | 429 | A rate limit was hit; Retry-After says how long | Wait; do not retry in a loop |
PLATFORM_READ_ONLY | 503 | Writes are paused platform-wide; reads still work | Wait Retry-After, retry the write once |
PLATFORM_MAINTENANCE | 503 | The platform is down for maintenance | Wait Retry-After, then retry |
QUERY_FAILED | 500 | The read or write could not complete — never an empty 200 | Retry; if it persists, contact support with the time and route |
Not found is deliberate
The API never distinguishes "does not exist" from "not published" to a caller without draft access — doing so would let a read token discover draft work. It also checks the credential before resolving the URL, so an anonymous caller cannot tell a real project slug from an invented one.
Writes are all-or-nothing
Every write validates the whole payload before touching anything: a 422, a 409 VERSION_LOCKED or a 409 SLUG_CONFLICT means nothing was written. A create whose content commit fails rolls the new row back, so a retry never lands on title-2.