Rate limits and errors
Errors
Every error has the same shape, and code is stable (the message is meant for people and may change):
{"error": {"code": "missing_scope", "message": "Esta clave no tiene el alcance «ptz».", "status": 403,
"request_id": "9b1c…", "required_scope": "ptz"}}
Messages are in Spanish; build your logic on code. A validation error includes fields ({"name": ["El campo nombre es obligatorio."]}), a 429 includes retry_after (and the Retry-After header), and a clash with a control lease includes lease (who holds it and until when). Every response, successful or not, carries X-Request-Id.
| Status | code |
What to do |
|---|---|---|
| 400 | key_in_query |
The key goes in the header. Rotate it: it ended up in logs |
| 401 | invalid_key, key_revoked, key_expired, creator_suspended, key_inactive |
Create a new key, or reactivate its creator |
| 403 | missing_scope |
The key needs the scope named in required_scope |
| 403 | forbidden, space_suspended |
The role is not enough, or the space is suspended |
| 404 | not_found |
It doesn't exist or this key can't see it: deliberately the same answer |
| 409 | conflict, camera_refused |
The camera can't do it right now (offline, another feature prevents it) |
| 409 | camera_leased |
Another key holds control of the camera |
| 409 | lease_held |
You tried to take or release a lease held by another key |
| 409 | lease_hold_limit |
Your key held that camera for 30 minutes in a row: wait retry_after seconds |
| 409 | lease_revoked_by_user |
A person released the camera from the panel: your key can't take it for retry_after seconds |
| 409 | kill_switch, automation_inactive, not_started, automation_refused |
The automation can't run or change right now |
| 410 | gone |
The camera is being decommissioned |
| 422 | validation_failed, no_manual_trigger |
Invalid data (fields), or the automation has no Manual trigger |
| 429 | rate_limited, automation_capped |
Wait for Retry-After |
| 5xx | internal_error, upstream_error, unavailable |
Retry only what can't duplicate (see below) |
Rate limits
| What | Limit |
|---|---|
| Requests per key | Whatever was chosen when it was created: 1 to 600 per minute (120 by default) |
| Requests per IP address, before the key is checked | 1200 per minute |
PTZ (move and stop) |
400 per minute per person, like the panel's pad |
| Live video and talk | 30 per minute per person |
| File links (snapshots and recordings) | 240 per minute |
| Camera commands | 60 per minute per person |
| Camera provisioning | 10 per hour per space; credential rotation, 6 |
| Control lease (take, renew, release, check) | 60 per minute per key |
| Running automations | 30 per minute per key, plus the engine's own caps: 12 runs per minute and 3 open at once per automation (automation_capped) |
| Testing the bucket and pinging a webhook | 10 per minute per key |
Keys and panel sessions belonging to the same person share the per-person limits.
Retries
- On a 429, wait for
Retry-Afterand try again: the server did nothing. - On a 5xx or a dropped connection, retry only what can't duplicate:
GET,PUT,DELETE, aPOSTwith anIdempotency-Key, renewing a lease, turning an automation on or off. Don't blindly retryPOST /automations/{id}/run: it could run twice. - The SDKs already behave this way, with exponential backoff (0.5 s, 1 s, 2 s… up to 8 s) plus some jitter.
Idempotency
POST /cameras and POST /webhooks accept an Idempotency-Key. Repeating a request with the same one (within 24 h, with the same API key) returns the same response with the Idempotent-Replayed: true header, without creating anything new. With a different body you get 422 idempotency_mismatch; if the first request is still running, 409 idempotency_in_progress. The SDKs set one on every creation call.
Pagination
Lists accept ?page= and ?per_page= (up to 100) and respond with {"data": [...], "meta": {"page": 1, "per_page": 25, "has_more": true}}. cameras.all(), automations.all() and paginate() walk every page for you.