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.
{
"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, 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_scenario400 | 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_key401 | The key is missing, malformed or unknown.Send Authorization: Bearer <key> with a key from the dashboard. |
expired_api_key401 | The key passed its expiry date.Create a new key. |
revoked_api_key401 | The key was revoked in the dashboard. Revocation takes effect within 15 seconds.Create a new key. |
insufficient_scope403 | The key lacks the scope this operation needs.Use a key with that scope. |
feature_not_entitled403 | The project's plan includes none of the capabilities this operation serves.Change plan in the dashboard. |
mode_mismatch403 | 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_found404 | No such room, device or other resource in this project and mode, or no such API route.Check the ID and path. |
configuration_conflict409 | 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_conflict409 | The configuration changed since you read it.Read it again, then retry with the new expected_revision. |
idempotency_conflict409 | 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_expired410 | A stream's Last-Event-ID is invalid or older than the replay window.Read a fresh snapshot and backfill from history. |
data_expired410 | A pagination cursor outlived the snapshot it pages through.Start the list again from the first page. |
rate_limit_exceeded429 | 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_exceeded429 | 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_unavailable503 | 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_unavailable503 | 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_error500 | 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-Idstring | headerOn every response. The same value as meta.request_id. |
RateLimit-Limitinteger | headerRequests per minute allowed for the key. On every authenticated response. |
RateLimit-Remaininginteger | headerRequests left in this minute, for the key and its project, whichever is lower. |
RateLimit-Resetinteger | headerSeconds until the one-minute window resets. |
Retry-Afterinteger | headerSeconds 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 characters | headerThe 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. |