Skip to content

Camera endpoints

All endpoints live under /api/v1/cameras. Read endpoints are open to every authenticated role; viewers are site-scoped (D78). Write endpoints require Operator or above (D81). Manual triggers rate-limit to one call per camera per 5 seconds (D49).

GET /api/v1/cameras

List cameras in the caller’s org.

Since: v1.0 Required role: viewer (site-scoped)

Query parameters

NameTypeDefaultConstraints
site_iduuid—Filters to the given site
gateway_iduuid—Filters to cameras bound to a gateway
statusenum—online, offline, pending, error, disabled
typeenum—rtsp, onvif
connection_modeenum—gateway, direct
cursorstringnullPagination cursor
limitint201-100

Response 200

{
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Loading Dock East",
"site_id": "b5e9f3a1-2c4d-4e6f-8a1b-3c5d7e9f1a2b",
"gateway_id": "8f1a2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
"connection_mode": "gateway",
"type": "onvif",
"status": "online",
"active_tier": "main"
}
],
"next_cursor": null,
"total": 1
}

Error responses

CodeBody.errorMeaning
401auth_requiredMissing or invalid token
403forbiddenRole insufficient
429rate_limitedSee /developer/rate-limits

Examples

Terminal window
curl -H "Authorization: Bearer sk_live_abc123" \
"https://novavms.novalien.com/api/v1/cameras?site_id=b5e9f3a1-2c4d-4e6f-8a1b-3c5d7e9f1a2b"
const cameras = await novavms.cameras.list({
siteId: 'b5e9f3a1-2c4d-4e6f-8a1b-3c5d7e9f1a2b',
});
cameras = client.cameras.list(site_id="b5e9f3a1-2c4d-4e6f-8a1b-3c5d7e9f1a2b")

POST /api/v1/cameras

Create a camera. Gateway-bound cameras validate ONVIF/RTSP credentials synchronously before returning; on failure the camera is rolled back and an error is returned. Direct-mode cameras register with go2rtc asynchronously.

Since: v1.0 Required role: operator

Request body

NameTypeDefaultConstraints
site_iduuid—Required
gateway_iduuid—Required when connection_mode=gateway
connection_modeenumgatewaygateway, direct
namestring—1-100 chars
typeenum—rtsp, onvif
rtsp_urlstring—Required for type=rtsp
rtsp_usernamestring—Optional
rtsp_passwordstring—Optional
hoststring—Required for type=onvif
portint80ONVIF service port
onvif_usernamestring—Required for type=onvif
onvif_passwordstring—Required for type=onvif

Response 201

{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Loading Dock East",
"site_id": "b5e9f3a1-2c4d-4e6f-8a1b-3c5d7e9f1a2b",
"gateway_id": "8f1a2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
"connection_mode": "gateway",
"type": "onvif",
"status": "online",
"created_at": "2026-04-21T14:05:00Z"
}

Error responses

CodeBody.errorMeaning
400validation_errorMissing field or gateway connection check failed
403forbiddenRole insufficient
404not_foundSite or gateway not in caller’s org

Examples

Terminal window
curl -X POST https://novavms.novalien.com/api/v1/cameras \
-H "Authorization: Bearer sk_live_abc123" \
-H "Content-Type: application/json" \
-d '{"site_id":"b5e9f3a1-2c4d-4e6f-8a1b-3c5d7e9f1a2b","gateway_id":"8f1a2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c","connection_mode":"gateway","name":"Loading Dock East","type":"onvif","host":"10.0.0.63","port":2020,"onvif_username":"admin","onvif_password":"s3cret"}'
const cam = await novavms.cameras.create({
siteId: 'b5e9f3a1-2c4d-4e6f-8a1b-3c5d7e9f1a2b',
gatewayId: '8f1a2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c',
connectionMode: 'gateway',
name: 'Loading Dock East',
type: 'onvif',
host: '10.0.0.63',
port: 2020,
onvifUsername: 'admin',
onvifPassword: 's3cret',
});
cam = client.cameras.create(
site_id="b5e9f3a1-2c4d-4e6f-8a1b-3c5d7e9f1a2b",
gateway_id="8f1a2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
connection_mode="gateway",
name="Loading Dock East",
type="onvif",
host="10.0.0.63",
port=2020,
onvif_username="admin",
onvif_password="s3cret",
)

GET /api/v1/cameras/{id}

Return a single camera.

Since: v1.0 Required role: viewer (site-scoped)

Response 200

{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Loading Dock East",
"site_id": "b5e9f3a1-2c4d-4e6f-8a1b-3c5d7e9f1a2b",
"gateway_id": "8f1a2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
"connection_mode": "gateway",
"type": "onvif",
"status": "online",
"stream_transport": "tcp",
"active_tier": "main",
"created_at": "2026-04-21T14:05:00Z"
}

Error responses

CodeBody.errorMeaning
401auth_requiredMissing or invalid token
403forbiddenViewer outside site scope
404not_foundCamera not in caller’s org

Examples

Terminal window
curl -H "Authorization: Bearer sk_live_abc123" \
https://novavms.novalien.com/api/v1/cameras/550e8400-e29b-41d4-a716-446655440000
const cam = await novavms.cameras.get('550e8400-e29b-41d4-a716-446655440000');
cam = client.cameras.get("550e8400-e29b-41d4-a716-446655440000")

PATCH /api/v1/cameras/{id}

Update camera metadata or credentials.

Since: v1.0 Required role: operator

Request body

NameTypeDefaultConstraints
namestring—1-100 chars
site_iduuid—Must exist in caller’s org
rtsp_urlstring—Triggers revalidation
rtsp_usernamestring—Triggers revalidation
rtsp_passwordstring—Triggers revalidation
onvif_usernamestring—Triggers revalidation
onvif_passwordstring—Triggers revalidation

Response 200

Returns the updated camera object — same shape as GET /api/v1/cameras/{id}.

Error responses

CodeBody.errorMeaning
400validation_errorCredential change failed revalidation
403forbiddenRole insufficient
404not_foundCamera not in caller’s org

Examples

Terminal window
curl -X PATCH https://novavms.novalien.com/api/v1/cameras/550e8400-e29b-41d4-a716-446655440000 \
-H "Authorization: Bearer sk_live_abc123" \
-H "Content-Type: application/json" \
-d '{"name":"Loading Dock East (Rev 2)"}'
await novavms.cameras.update('550e8400-e29b-41d4-a716-446655440000', {
name: 'Loading Dock East (Rev 2)',
});
client.cameras.update(
"550e8400-e29b-41d4-a716-446655440000",
name="Loading Dock East (Rev 2)",
)

DELETE /api/v1/cameras/{id}

Soft-delete the camera. Events and recordings are preserved; the camera stops accepting streams.

Since: v1.0 Required role: operator

Response 204

Empty body.

Error responses

CodeBody.errorMeaning
403forbiddenRole insufficient
404not_foundCamera not in caller’s org

Examples

Terminal window
curl -X DELETE https://novavms.novalien.com/api/v1/cameras/550e8400-e29b-41d4-a716-446655440000 \
-H "Authorization: Bearer sk_live_abc123"
await novavms.cameras.delete('550e8400-e29b-41d4-a716-446655440000');
client.cameras.delete("550e8400-e29b-41d4-a716-446655440000")

POST /api/v1/cameras/{id}/trigger

Manually record a clip (“Record Clip” button, D49). One trigger per camera per 5 seconds.

Since: v1.2 Required role: operator

Request body

NameTypeDefaultConstraints
duration_secint155-60
notestring—Up to 500 chars; stored on the event

Response 202

{
"event_id": "c1d2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
"started_at": "2026-04-21T14:10:00Z",
"expected_clip_duration_sec": 15
}

Error responses

CodeBody.errorMeaning
400ai_unavailableCamera is not online
403forbiddenRole insufficient
404not_foundCamera not in caller’s org
429rate_limitedRetry-After header set to 5 seconds

Examples

Terminal window
curl -X POST https://novavms.novalien.com/api/v1/cameras/550e8400-e29b-41d4-a716-446655440000/trigger \
-H "Authorization: Bearer sk_live_abc123" \
-H "Content-Type: application/json" \
-d '{"duration_sec":30,"note":"Manager requested walkthrough"}'
const ev = await novavms.cameras.triggerRecording(
'550e8400-e29b-41d4-a716-446655440000',
{ durationSec: 30, note: 'Manager requested walkthrough' },
);
ev = client.cameras.trigger_recording(
"550e8400-e29b-41d4-a716-446655440000",
duration_sec=30,
note="Manager requested walkthrough",
)

Sub-routes under /cameras/{id} (pointer)

The camera handler exposes additional live endpoints not yet broken out into per-section detail on this page. Each has a corresponding entry in docs/arch-api-spec.md §14 Cameras. All require a Bearer access token; site-access checks mirror GET /cameras/{id}.

  • GET /api/v1/cameras/{id}/diagnostics — debug bundle (Phase 6 device diagnostics). Handler: cloud/internal/devices/camera_handler.go:128.
  • GET /api/v1/cameras/{id}/health — current online / offline / error tier. Handler: camera_handler.go:129.
  • GET /api/v1/cameras/{id}/thumbnail — 200 PNG, 1-hour signed-storage URL. Handler: camera_handler.go:130.
  • POST /api/v1/cameras/{id}/snapshot — request an on-demand snapshot from the gateway. Handler: camera_handler.go:144.
  • POST /api/v1/cameras/{id}/unpair — remove the camera from its current gateway (does not soft-delete). Handler: camera_handler.go:138.
  • POST /api/v1/cameras/validate-connection — pre-check ONVIF / RTSP credentials before creating the camera. Handler: camera_handler.go:140.
  • POST /api/v1/cameras/{id}/ptz — pan / tilt / zoom continuous-move command (D20-RESTORE). Handler: cloud/internal/devices/ptz_handler.go:49.
  • GET /api/v1/cameras/{id}/analysis-focus, PUT /api/v1/cameras/{id}/analysis-focus, GET /api/v1/cameras/{id}/analysis-focus/history — per-camera AI analysis-focus zones (Phase 6). Handler: cloud/internal/devices/analysis_focus_handler.go.
  • GET /api/v1/cameras/{id}/nbd-override, PATCH /api/v1/cameras/{id}/nbd-override, DELETE /api/v1/cameras/{id}/nbd-override — per-camera NBD knob overrides (Phase 4.5, migration 000080). Handler: cloud/internal/devices/nbd_override_handler.go.
  • GET /api/v1/cameras/{id}/recording-coverage, GET /api/v1/cameras/{id}/recordings, GET /api/v1/cameras/{id}/clip-at — playback / scrubbing sub-routes. Handler: cloud/internal/events/handler.go:1135-1137.
  • GET /api/v1/analysis-focus-presets — list available analysis-focus presets (org-wide, not per-camera). Handler: analysis_focus_handler.go:36.

The OpenAPI yaml at /openapi.yaml does not yet inventory these routes (see Phase 8 GAP-8-16). The Phase 8-12 follow-ups track adding them.