API and automations: how they coexist
A space can have automations (set up in the panel: "on motion, go to the door and take a snapshot") and, at the same time, an integration using the API. Using the API doesn't switch anything off. Both go through the same command path, with the same conflict rules, and every change is attributed to whoever made it.
One command path
Whatever the API asks for, whatever a person does in the panel and whatever an automation does all reach the camera the same way: the same allowed commands, the same conflicts ("you can't track people while patrol is on"), the same rate limits and the same audit log. The API has no shortcut.
Who did what
Every change records its actor:
| Actor | Who |
|---|---|
user:<id> |
A person in the panel or the app |
automation:<id> |
An automation |
api:<key name> |
An integration, through its key |
You'll see it in the audit log (meta.actor), on every command (actor in GET /cameras/{id}/commands/{command}) and on camera events caused by an API request (details.actor in webhooks). That's why the key name should say which system it is.
Moving through the API is moving by hand
A PTZ move through the API (a step, going to a position, home or a saved preset) counts as a move by a person:
- Person tracking pauses for one minute, just like when someone uses the panel's pad.
- The camera's patrol pauses for its configured time ("Pausa después de moverla a mano", the pause after a manual move) and then resumes on its own.
If you need the camera to stay put longer than that, or no automation to move it while your system is using it, take a control lease.
The control lease
POST /cameras/{id}/lease with the control scope (and permission to operate the camera) gives your key control for ttl_seconds, from 10 to 300 (60 by default). While it lasts:
- Automation actions that move or change that camera are skipped: PTZ, presets, patrol, recording, streaming, settings changes (detection, "Hola Tero", watermark, scheduled snapshots), tracking and face blurring. The step ends up
skippedwith codeleasedand the reason ("en control por la API «name» hasta las 14:05" — under API control until 14:05), and the automation shows its latest skip. - The rest of the flow keeps running. Anything that doesn't move or change the camera runs as usual: taking a snapshot, saying a phrase, running detection once, adding a timelapse frame, notifying, sending an email or a webhook. The following steps receive
skipped: "leased", in case a condition wants to branch on it. - Tracking doesn't move it.
- Other keys can't move it or change its recording (409
camera_leased), nor take control (409lease_held). Watching it, taking snapshots and opening video still work. - People are never blocked. The camera page shows "En control por la API «name»" (under API control), and anyone allowed to configure the camera can release it ("Liberar").
Rules:
- One per camera. Requesting it again with the same key renews it: expiry becomes now +
ttl_seconds. It answers 201 when taken and 200 when renewed. - It's capped. A key holds a camera for at most 30 minutes in a row (
hold_ends_atin the response). Renewing never pushesexpires_atpast that cap, and releasing and taking it again within 5 minutes counts as in a row. Once the cap is reached, that key waits 5 minutes before it can take that camera again (409lease_hold_limit, withretry_after,blocked_untiland theRetry-Afterheader). Other keys can take it. - Releasing from the panel blocks the key. If a person releases it from the panel ("Liberar"), that key can't take that camera again for 10 minutes (409
lease_revoked_by_user). It's written to the audit log (device.lease_blocked). Other cameras and other keys are unaffected. - It expires on its own. If your system goes down, the camera returns to the automations after
ttl_seconds. Renew at a third of the TTL (holddoes it for you). - It dies with the key. A revoked or expired key holds nothing, and a key from another space can't even see the camera (404).
- The three durations are platform settings (
DEVAPI_LEASE_MAX_HOLD_SECONDS,DEVAPI_LEASE_COOLDOWN_SECONDS,DEVAPI_LEASE_RELEASE_BLOCK_SECONDS); the ones above are the defaults. - It's recorded. Taking and releasing it write an audit row and a
lease.taken/lease.releasedcamera event (also sent to webhooks ascamera.lease.*).
TypeScript / JavaScript — sdk/js/examples/portal/leases.ts
// Take control for a while: automations will not move or change this camera until you release it.
try {
return await tv.leases.hold(cameraId, async ({ lease, signal }) => {
console.log(`in control as «${lease.holder}» until ${lease.expires_at}`);
for (const direction of ['left', 'left', 'up'] as const) {
if (signal.aborted) break; // a renewal failed: stop moving
await tv.ptz.move(cameraId, direction);
}
return tv.ptz.goto(cameraId, 1200, 300);
}, { ttlSeconds: 60 }); // renewed every 20 s, released at the end (also on errors)
} catch (err) {
if (err instanceof ConflictError && err.code === 'lease_held') {
console.log('another integration is driving this camera'); // err.body.error.lease says who and until when
return null;
}
throw err;
}
Managing automations
| Request | Scope | What it does |
|---|---|---|
GET /automations, GET /automations/{id} |
automations:read |
Name, whether it's on, paused or has errors, its triggers, whether it can be run, and its latest skip. No graph: that's edited in the panel |
POST /automations/{id}/enable, …/disable |
automations:write |
Same as the switch in the panel and the app. Turning it off stops whatever was running. A managed automation (created by a camera setting) turns its feature on or off in the camera, with the same conflicts |
POST /automations/{id}/run |
automations:run |
Runs it from its Manual trigger, with optional camera_id and data |
GET /automations/{id}/runs/{run} |
automations:read |
The run, step by step (status, code and note for each) |
All of these require the admin role, as in the panel. Graphs can't be edited through the API in this version.
Running from the API
- Only automations with a Manual trigger (
runnable: true). Without one, 422no_manual_trigger. - It's a real run, not a test: whatever the flow does on site (move, speak, record) actually happens.
- It follows the same rules as any run: the space's kill switch (409
kill_switch), the automation must be on (409automation_inactive), the engine's caps (429automation_capped) and loop protection. At every step the engine checks that whoever last saved the automation can still use that camera; if not, the step fails and stays in the history. datareaches the flow as plain data ({{ order }}in a message): up to 20 simple values, keys matching[A-Za-z][A-Za-z0-9_]*, strings up to 500 characters. It can't pick cameras, URLs or phrases the flow doesn't already have.- The trigger records
trigger.actor = "api:<name>"and the audit log recordsautomation.run_by_api.
Events go out through webhooks
Whatever happens on the camera in the meantime (it moved, patrol started, someone took or released control) reaches your system through the existing webhooks. No polling needed.