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_arguments400, 413, 422The 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_scenario400A 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_key401The key is missing, malformed or unknown.Send Authorization: Bearer <key> with a key from the dashboard.
expired_api_key401The key passed its expiry date.Create a new key.
revoked_api_key401The key was revoked in the dashboard. Revocation takes effect within 15 seconds.Create a new key.
insufficient_scope403The key lacks the scope this operation needs.Use a key with that scope.
feature_not_entitled403The project's plan includes none of the capabilities this operation serves.Change plan in the dashboard.
mode_mismatch403A 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_found404No such room, device or other resource in this project and mode, or no such API route.Check the ID and path.
configuration_conflict409The 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_conflict409The configuration changed since you read it.Read it again, then retry with the new expected_revision.
idempotency_conflict409An 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_expired410A stream's Last-Event-ID is invalid or older than the replay window.Read a fresh snapshot and backfill from history.
data_expired410A pagination cursor outlived the snapshot it pages through.Start the list again from the first page.
rate_limit_exceeded429Too 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_exceeded429A 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_unavailable503This 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_unavailable503A 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_error500An 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-IdstringheaderOn every response. The same value as meta.request_id.
RateLimit-LimitintegerheaderRequests per minute allowed for the key. On every authenticated response.
RateLimit-RemainingintegerheaderRequests left in this minute, for the key and its project, whichever is lower.
RateLimit-ResetintegerheaderSeconds until the one-minute window resets.
Retry-AfterintegerheaderSeconds 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-Clientstring, up to 100 charactersheaderThe 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.