# Errors

> Standard HTTP status codes, one body shape and a stable code for each failure, so you can handle them in one place.

## Error shape

Every error is `{ error, meta }`. Branch on `error.code`, never on the message, which can change. `details` lists each invalid field when validation fails. Quote `meta.request_id` when you contact support; it is also the `X-Request-Id` header and appears in your dashboard logs.

`422 Unprocessable Content`

```
{
  "error": {
    "code": "invalid_arguments",
    "message": "Invalid input",
    "details": [{ "path": "/limit", "message": "must be <= 1000" }]
  },
  "meta": {
    "request_id": "req_01",
    "project_id": "proj_demo",
    "livemode": false,
    "generated_at": "2026-10-24T23:55:30.000Z"
  }
}
```

## Codes

- `invalid_arguments` · 400, 413, 422 — The request is malformed or fails validation: 400 for a body that isn't JSON or a malformed path, 413 for a body over 1 MiB, 422 for an unknown, duplicate or invalid parameter or field. · Fix the request; details lists each invalid field.
- `invalid_scenario` · 400 — A scenario run can't be started or controlled: an unknown scenario, a timeline step missing its room, track, type or kind, or an unknown control action. · Fix the timeline or control request named in the message.
- `invalid_api_key` · 401 — The key is missing, malformed or unknown. · Send Authorization: Bearer <key> with a key from the dashboard.
- `expired_api_key` · 401 — The key passed its expiry date. · Create a new key.
- `revoked_api_key` · 401 — The key was revoked in the dashboard. Revocation takes effect within 15 seconds. · Create a new key.
- `insufficient_scope` · 403 — The key lacks the scope this operation needs. · Use a key with that scope.
- `feature_not_entitled` · 403 — The project's plan includes none of the capabilities this operation serves. · Change plan in the dashboard.
- `mode_mismatch` · 403 — A live key called a test-only operation, such as a scenario run. The SDK raises the same code when a response's mode differs from the mode it expects. · Use a test key for simulator controls.
- `resource_not_found` · 404 — No such room, device or other resource in this project and mode, or no such API route. · Check the ID and path.
- `configuration_conflict` · 409 — The request clashes with the current state, such as advancing a run that isn't paused or deleting a scene in use. · Resolve the state named in the message, then retry.
- `revision_conflict` · 409 — The configuration changed since you read it. · Read it again, then retry with the new expected\_revision.
- `idempotency_conflict` · 409 — An Idempotency-Key was reused for a different request, or the first request with that key is still in progress. · Use a new key for a new request; retry an in-progress one shortly.
- `replay_expired` · 410 — A stream's Last-Event-ID is invalid or older than the replay window. · Read a fresh snapshot and backfill from history.
- `data_expired` · 410 — A pagination cursor outlived the snapshot it pages through. · Start the list again from the first page.
- `rate_limit_exceeded` · 429 — Too many requests this minute for the key (or twice that across the project), or too many open streams for the project. · Retry after the seconds in Retry-After.
- `quota_exceeded` · 429 — A live key's project has used its monthly live-hour cap. There is no Retry-After. · Don't retry; raise the live-hour cap in the dashboard.
- `live_unavailable` · 503 — This operation has no live data for this project yet (for example, no paired Cymo covers it). · Don't retry the same request; pair a Cymo that covers it, or build against a test key.
- `service_unavailable` · 503 — A dependency, such as API key verification, is unreachable, or the project is busy with another write. · Retry after the seconds in Retry-After, with backoff.
- `internal_error` · 500 — An unexpected failure inside the API. It is reported to us with the request ID. · Retry with backoff; contact support with the X-Request-Id if it persists.

The set is open: keep unknown codes and fall back to the HTTP status.

## When to retry

`429` has two codes that need opposite handling. `rate_limit_exceeded` clears on its own: wait the seconds in `Retry-After` and send the request again. `quota_exceeded` means a live project used its monthly live-hour cap; it carries no `Retry-After` and won't succeed until someone raises the cap in the dashboard.

`503` also has two. `service_unavailable` is transient: retry after `Retry-After` with backoff. `live_unavailable` means the operation has no live data for this project, for example because no paired Cymo covers it, so retrying won't help.

Retry writes with the same `Idempotency-Key` so they can't happen twice. The SDK follows these rules for you: it retries reads and idempotent requests after network failures and `502`, `503` and `504`, retries `rate_limit_exceeded` after `Retry-After` up to 30 seconds, and never retries `quota_exceeded` or `live_unavailable`. Its errors expose `status`, `code`, `request_id`, `retry_after_ms`, `details` and the `RateLimit-*` values.

## Headers

Responses carry a request ID and your rate-limit budget. Requests are limited per key in fixed one-minute windows, and to twice that across the project.

- `X-Request-Id` · string — header · On every response. The same value as meta.request\_id.
- `RateLimit-Limit` · integer — header · Requests per minute allowed for the key. On every authenticated response.
- `RateLimit-Remaining` · integer — header · Requests left in this minute, for the key and its project, whichever is lower.
- `RateLimit-Reset` · integer — header · Seconds until the one-minute window resets.
- `Retry-After` · integer — header · Seconds to wait before retrying. Sent with rate\_limit\_exceeded and service\_unavailable, never with quota\_exceeded.

Requests may name the client that sent them. The SDK, CLI and MCP server set it for you.

- `X-Cymo-Client` · string, up to 100 characters — header · The calling surface and version, such as sdk/0.3.0 or cli/0.2.0\. Request logs group requests by its prefix: sdk, cli, mcp or dashboard, and other for anything else.
