Keys and scopes
The key
A key is one person in one space: it acts as the person who created it, only in that space, with a role ceiling and only with the scopes it was given. It never adds permissions: permissions, groups, rate limits and auditing are the same as in the panel, and they are checked on every request.
- Format:
tv_live_<prefix>_<secret>. The prefix (12 characters) identifies the key and is visible in the panel; the secret (48 characters) is compared in constant time. Only a hash of the secret is stored. - Where it goes: only in
Authorization: Bearer. In the URL (?api_key=,?token=…) it is rejected with 400key_in_query, because URLs end up in every proxy's logs. If you already sent it that way, rotate it. - Where it never goes: a browser, or an app someone can open up. To show video on a web page, your server requests the session and passes only the view token to the browser (see Guides).
The role
You pick a maximum role when creating the key (admin, member or view only), and the key gets the lower of that and the role its creator has today. If the creator is downgraded, the key goes down with them.
| Role | What it can do |
|---|---|
| View only | See the cameras in its groups. Cannot move, talk or take control. |
| Member | See and operate the cameras in its groups (move, snapshots, recording, talk, take control). |
| Admin | All cameras, configuring them, the bucket, webhooks and automations. |
Scopes
Without the scope, the route answers 403 missing_scope (with required_scope) even if the role would allow it.
| Scope | Allows |
|---|---|
cameras:read |
Listing cameras, their details and status, and command results |
cameras:write |
Provisioning, decommissioning and rotating credentials (also requires admin) |
ptz |
Moving, stopping, home and presets (saving and deleting presets requires admin) |
live |
Opening live video (WebRTC and HLS) |
talk |
Talking through the camera |
snapshots |
Requesting and viewing snapshots |
recordings |
Recordings, and turning recording on or off (changing its settings requires admin) |
storage |
The space's bucket (admin) |
events |
Camera events |
webhooks |
Outgoing webhooks (admin) |
control |
Taking control of a camera for a few minutes (control lease) |
automations:read |
Viewing automations and their runs (admin) |
automations:write |
Turning them on and off (admin) |
automations:run |
Running the ones that have a Manual trigger (admin) |
Grant only what the integration needs: a key that only watches video needs neither ptz nor control.
When it stops working
On the very next request:
| If this happens | Response |
|---|---|
| It is revoked, from the panel or the app | 401 key_revoked |
| It expired | 401 key_expired |
| Its creator leaves the space (or is removed) | 401 key_revoked, and it stays revoked even if they rejoin |
| Its creator is suspended | 401 creator_suspended; it works again once they are reactivated |
| The space is suspended | 403 space_suspended |
A lost key cannot be recovered: create a new one and revoke the old one. A revoked key immediately gives up control of any camera it was holding.
What gets recorded
Every request that changes something writes an api.call row to the hash-chained audit log, with the key id and actor: api:<key name>. The same goes for every row the panel writes during that request (watching video, downloading a file, changing settings), every command sent to the camera (actor in GET /cameras/{id}/commands/{command}) and every camera event caused by the request. Reads write at most one api.read row per key and route every 5 minutes.
Every response carries X-Request-Id: use it to find the request in the audit log.