---
title: "Get Camera By Id"
method: GET
path: "/cameras/{camera_id}"
tags: ["cameras"]
---

# Get Camera By Id

`GET /cameras/{camera_id}`

## Request

- Base URL: `https://api.verdun.ai` (the document declares no server; this is the origin it was published from)
- URL: `https://api.verdun.ai/cameras/{camera_id}`
- Auth: HTTP bearer

## Path parameters

- `camera_id` string, required

## Response `200`

Successful Response

- GetCameraByIdResponse
  - `id` string, uuid, required: The camera ID.
  - `camera_label` string, required: The label of the camera.
  - `camera_id` string, required: The camera ID.
  - `kvs_stream_name` string, required: The KVS stream name.
  - `ip_address` string, required: The IP address of the camera.
  - `port` string, required: The port of the camera.
  - `username` string, required: Camera account username (the password is never returned).
  - `integration` 'RTSP' | 'Network Optix' | 'Axis' | 'Avigilon' | 'Milestone' | 'Verkada' | 'Turing' | 'Hikvision' | 'AvertX', required
  - `adapter` 'generic_onvif' | 'hanwha_onvif'
  - `brand` 'hanwha' | 'dahua'
  - `model` 'ANV-L7012R'
  - `metadata_format` 'onvif'
  - `rtsp_url` string, nullable: The camera's RTSP URL with its password redacted. Never the working credentialed URL. See EditCameraRequest.rtsp_url for the write contract (blank or a REDACTED-bearing echo means keep).
  - `rag_context` string, nullable: Free-form scene context passed to the AI pipeline for this camera.
  - `ingest_mode` 'continuous' | 'motion_clip'
  - `motion_config` object, nullable: Extensible motion settings with bounded known keys (timings, thresholds, detector geometry/rate and RTSP buffer mode); see app/services/motion_config_validation.py. Unknown keys are preserved.
  - `analytics_provider` 'none' | 'deepalert'
  - `analytics_config` object, nullable: Opaque per-vendor analytics settings interpreted by the ingestor.
  - `ii_enabled` boolean: Incident intelligence on/off; when false motion events still record and alert, but skip AI classification.
  - `fire_watch` boolean: Fire-screen gate; when true, analytics-rejected motion is still screened for smoke/flame before being discarded.
  - `monitoring_profile` 'security' | 'safety'
  - `live_view_only` boolean: Live View only: the camera is connected for Live View with no AI and no alarms (its monitoring state is always off). The site-level flag on the camera's site has the same effect.
  - `display_rotation` integer: Degrees counter-clockwise the recorded frame is turned for display (0 or 180). Set by an owner for an upside-down camera; clips are then stamped with a display-matrix tag and the camera's zones are stored in the rotated (upright) space.
  - `agent_mode` boolean: Agent mode on/off; when true the backend acts on AI recommendations (auto-dismiss / voice-down / escalate-upgrade) instead of holding every clip for human review.
  - `agent_observation_minutes` integer: Post-voice-down observation window in minutes before an alert auto-dismisses (agent mode).
  - `agent_incident_types` string[], nullable: Incident types the agent may process at all on this camera; unselected types go to the human queue. Auto-dismiss is additionally bounded by the global safety allowlist.
  - `incident_routing` object, nullable: Per-camera incident routing: incident type -> 'ignore' (dropped and auto-dismissed as not monitored) | 'humans' (operator queue) | 'agent'. A type absent from the map routes to 'humans'; UNKNOWN is never present. Supersedes agent_incident_types, which is now derived from this and kept for one release. Typed as plain strings on the RESPONSE side, deliberately: the read path fails closed (a mode outside the enum reads as 'humans'), and response validation must not undo that by 500-ing on a stored value this build does not know - a hand edit, or a newer task's mode during a rolling deploy. The enum is enforced on the REQUEST model only.
  - `detection_mode` string, nullable: DeepAlert Phase B routing mode (owner-only, written via PUT /cameras/{id}/detection-mode): 'legacy' | 'ii_gated'; null means legacy. Only meaningful when analytics_provider == 'deepalert'.
  - `ptz` boolean: Whether this camera is pan/tilt/zoom; when true the 'camera moved' health issue is suppressed.
  - `integration_id` string, uuid, nullable: The cloud-VMS connection (site_integrations.id) this camera was onboarded from; null for NVR and direct-RTSP cameras.
  - `external_device_id` string, nullable: The VENDOR's own camera id within that connection; null unless integration_id is set.
  - `integration_device_info` object, nullable: Snapshot of what the vendor reported about the device at import (name, model, site, codec); null unless integration_id is set.
  - `live_view_until` string, date-time, nullable: On-demand live view is active while now() < this timestamp (motion_clip cameras).
  - `healthy` boolean, nullable: The healthy status of the camera.
  - `last_healthcheck_at` string, date-time, nullable: The last healthcheck at of the camera.
  - `map_x` number, nullable: Normalized floor-plan x position (0..1) on the site map.
  - `map_y` number, nullable: Normalized floor-plan y position (0..1) on the site map.
  - `map_lat` number, nullable: Geographic latitude on the site map.
  - `map_lng` number, nullable: Geographic longitude on the site map.
  - `map_location_label` string, nullable: Operator-facing location prose shown on queue rows and mini-maps.
  - `map_heading` number, nullable: Direction the device faces, degrees clockwise from map-up; null = not aimed.
  - `group_ids` string[]: The LIVE camera groups this camera belongs to. Camera groups are site-scoped and a camera may be in several at once; membership alone changes nothing until a group is given a schedule.
  - `monitoring_source` MonitoringSourceModel: WHICH schedule target produced this camera's current monitoring state. Exists because a CAMERA GROUP can now silence a camera. "off" with no explanation sends an operator hunting through three places; this names the one whose edit would actually change the answer. Custom camera schedules override custom groups, which override the site. Group ties are deterministic, even when the site happens to agree.
    - `state` 'off' | 'monitored' | 'heightened', nullable: The effective mode resolved with this source; includes heightened, unlike monitoring_active.
    - `source` string, required: 'camera' = a camera-level schedule override (wins outright); 'site' = the site schedule; 'group' = a camera group's schedule; 'holiday' = the site is closed for a configured holiday, so an otherwise-off cell reads monitored (reported only when the layer actually flipped the answer; group_id/group_name are null); 'disarm' = a live text-to-disarm window, which beats every schedule; 'arm' = a live temporary TEST ARM (an operator holding this site's cameras on outside the schedule so an install or a camera move can be tested); it beats every schedule and loses only to 'disarm'; 'default' = no schedule anywhere, so always monitored.
    - `group_id` string, uuid, nullable: The camera group responsible; non-null only when source == 'group'.
    - `group_name` string, nullable: That camera group's display name, so a console can render 'off via Tenant A' without a second lookup. Non-null only when source == 'group'.
  - `deepalert_threshold` DeepAlertThresholdModel: The READ-ONLY, server-resolved DeepAlert sensitivity a camera runs at, built by the same resolver create-event uses, so the console shows exactly what api forwards to incident-intelligence (ii). It is a top-level key on a camera read and NEVER part of `analytics_config`, which stays the raw stored blob the form round-trips (a console must never serialize this block back into analytics_config). A missing block means UNKNOWN (an older backend), not 'inherits the fleet default'.
    - `path` 'ii' | 'ingestion' | 'none', required: 'ii' = the camera is ii_gated (incident intelligence owns the DeepAlert gate); 'ingestion' = a DeepAlert camera on the legacy / II-off path, served by the ingestion server's own validator; 'none' = no DeepAlert.
    - `effective` number, nullable: The number api forwards. ii path: the valid override, else the fleet default, so always a number. ingestion path: the valid override, else null (api does not know the ingestion default). none: null.
    - `source` 'camera' | 'fleet_default' | 'invalid_override' | 'ingestion_default', nullable: 'camera' = a valid per-camera override; 'fleet_default' = no override, the fleet default applies (ii path); 'invalid_override' = the stored value is unusable and the fleet default applies (ii path; saving the camera form needs a deliberate repair); 'ingestion_default' = no valid override on the ingestion path, the ingestion server's own default applies (not reported). Null when path is 'none'.
    - `fleet_revision` integer, nullable: The fleet-default revision in force (ii path only), else null.
    - `override` number, nullable: The stored per-camera override when it is a VALID number, else null.
    - `override_valid` boolean: True iff a valid per-camera override is stored.
    - `invalid_override` string, nullable: A short repr of a stored but unusable override, so the console can say 'stored value X is invalid'; null otherwise. Never forwarded to the vendor.

## Other responses

- `422`: Validation Error

---

[API](https://skmtc.dev/verdun/apis/verdun-api.md) - [All operations](https://skmtc.dev/verdun/apis/verdun-api/llms.txt) - [OpenAPI document](https://skmtc.dev/verdun/apis/verdun-api/revisions/35a2a6ebedda?raw)
