TEASEDocs
Essentials

Errors

The consolidated catalog of stable error codes your client switches on, mapped to HTTP statuses with the trigger and the fix for each.

Every non-2xx response carries the same small JSON body: a stable, machine-readable error code and a human-readable message. This page is the catalog of the error codes worth branching on, and the HTTP statuses they ride on.

{
  "error": "invalid_scope",
  "message": "Invalid scope values: ['admin']. Allowed: ['read', 'write']"
}

Switch on the HTTP status and the error code — never on the message text. The message is for humans and may be reworded at any time; the status and error code are the contract. See Response structure.

HTTP statuses

Every endpoint draws from this table. Individual reference pages call out any status that carries a special meaning for that route.

StatusMeaningTriggered byWhat to do
400Bad requestThe body or query failed validation, or a read needs a connected account that is not yet connected.Read error / message, fix the request, then retry. Do not blindly retry — it fails identically.
401UnauthorizedMissing, malformed, or revoked bearer token.Check the Authorization header; mint a new key if it was revoked.
403ForbiddenValid token, but it lacks the scope for this action.Use a key with the required scope (for example a write key for a mutation).
404Not foundThe resource does not exist or is owned by another account.Confirm the id. Another account's resource is indistinguishable from one that never existed — by design.
409ConflictThe request collides with current state (uniqueness or a state-machine rule).Re-read current state, reconcile, then retry.
413Payload too largeAn uploaded file or request body exceeds the size cap.Shrink or compress the payload below the limit and retry.
422Unprocessable entityThe body parsed but a field is semantically invalid (wrong type, out of range, failed a rule).Fix the offending field named in message, then retry.
429Too many requestsYou exceeded a rate limit.Back off and retry — see Rate limits.
503Service unavailableA dependency is briefly unavailable or warming up.Transient — retry with backoff. If it persists, check status before assuming a client bug.

404 hides ownership

A resource that belongs to a different account returns 404, exactly like one that never existed. This is deliberate tenant isolation: you can never probe whether another account's id is real.

Error code catalog

These are stable error codes a client is likely to branch on. The list is not exhaustive — new codes can appear — so always keep a default branch that falls back to the HTTP status.

error codeStatusTriggerFix
invalid_scope400A request asked for a scope value that is not allowed.Request only read / write scopes when minting a key.
api_key_not_found404An operation referenced an API key id that does not exist or is not yours.Confirm the key id from the keys list; it may already be revoked.
domain_taken409You tried to claim a custom domain that is already in use.Choose a different domain, or release the existing claim first.
segment_has_buttons409You tried to delete a segment that still has buttons attached to it.Remove or reassign the segment's buttons, then delete the segment.
rate_limited429You exceeded the per-account rate limit.Back off with exponential delay and jitter, then retry the same request.

Treat this catalog as a starting set, not a closed enum. When you encounter an error code you do not recognize, fall through to handling by HTTP status — that always works.

Branch on status and code, not message

A correct client checks the status first, parses the consistent { error, message } body, switches on the stable error code, and keeps a default that handles by status.

const res = await fetch(url, { headers });
if (!res.ok) {
  const { error, message } = await res.json();
  switch (error) {
    case 'rate_limited':
      return backoffAndRetry();
    case 'invalid_scope':
      throw new Error('use a key with the required scope');
    case 'domain_taken':
      return promptForDifferentDomain();
    case 'segment_has_buttons':
      throw new Error('remove the segment buttons before deleting it');
    default:
      // unknown code — fall back to the HTTP status
      if (res.status >= 500) return backoffAndRetry();
      throw new Error(`${res.status} ${error}: ${message}`);
  }
}
const data = await res.json();
res = requests.get(url, headers=headers)
if not res.ok:
    body = res.json()
    error, message = body["error"], body["message"]
    if error == "rate_limited":
        return backoff_and_retry()
    if error == "invalid_scope":
        raise RuntimeError("use a key with the required scope")
    if error == "domain_taken":
        return prompt_for_different_domain()
    if error == "segment_has_buttons":
        raise RuntimeError("remove the segment buttons before deleting it")
    # unknown code — fall back to the HTTP status
    if res.status_code >= 500:
        return backoff_and_retry()
    raise RuntimeError(f"{res.status_code} {error}: {message}")
data = res.json()

Next steps

  • Response structure — the error shape and status table.
  • Rate limits — how to back off correctly on 429.
  • SDKs & tooling — a wrapper that raises a typed error carrying the error code.
  • FAQ — quick answers, including why some reads return 400 before you connect.

On this page