---
title: "Get Camera Health Analytics"
method: GET
path: "/analytics/camera-health"
tags: ["analytics"]
---

# Get Camera Health Analytics

`GET /analytics/camera-health`

## 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/analytics/camera-health`
- Auth: HTTP bearer

Per-camera connectivity, image quality and stream health for the whole
in-scope fleet — the Analytics -> Camera Health tab.

Scoped exactly like /summary above: an explicit parent_id outside the
operator's grants is rejected rather than silently emptied, the per-site
gap check closes ?site_id= for a narrowed manager, and the per-site
TenantScope goes to the service, where it becomes the ONLY tenant filter
on the fleet query (every other statement keys off the ids it returns).

## Query parameters

- `parent_id` string, nullable
- `site_id` string, nullable

## Response `200`

Successful Response

- GetCameraHealthAnalyticsResponse
  - `generated_at` string, date-time, required: Single server instant the whole response was computed at; every expires_in is relative to it.
  - `thresholds` CameraHealthThresholds, required: The server's issue thresholds, published on every response so the UI renders the same numbers the server decided with and the two can never drift. Never hardcode these client-side.
    - `sharpness_min` number, required: Sharpness below this raises the 'blur' issue.
    - `occlusion_pct_max` number, required: Occlusion above this raises the 'occlusion' issue.
    - `shift_pct_max` number, required: Shift above this raises the 'moved' issue (suppressed on PTZ cameras).
    - `frozen_identical_run` integer, required: Consecutive identical frames at or above this raise the critical 'frozen' issue.
    - `luminance_min` number, required: Luminance below this raises the 'dark' issue.
    - `luminance_max` number, required: Luminance above this raises the 'bright' issue.
    - `reconnects_max` integer, required: Total 24h reconnects at or above this raise the 'flapping' issue.
    - `bitrate_spike_ratio` number, required: Latest bitrate at or above this multiple of the 24h average raises the informational 'bitrate_spike' issue.
    - `camera_down_stale_seconds` integer, required: Armed with no fresh frame for at least this long (and telemetry itself still arriving) raises the critical 'camera_down' issue; also the basis for judging whether the camera's last monitored window closed dark. Sticky: stays set after the window ends and clears only when a fresh frame arrives while the camera is armed — an off-schedule frame (e.g. a live-view heartbeat) does not prove the monitored-window outage is over and does not clear it.
    - `view_obstructed_coverage_min` integer, required: An AI view check counts toward the 'obstructed' issue only when it reports the view obstructed, the kind is not 'glare', and coverage_pct is at least this.
    - `view_check_confirm_hours` integer, required: The 'obstructed' issue needs the latest AI view check AND the most recent check received at least this many hours before it (server clock) to both count; a single positive check raises nothing.
    - `view_flag_quiet_days` integer, required: Operator view flags (GET /camera-health/flags): after an operator clears a flag, the camera raises no new flag for this many days unless it gets worse.
    - `view_flag_worse_by_pct` integer, required: Operator view flags: inside the quiet period a new flag opens only when the obstruction covers at least this many percentage points more of the frame than the cleared one did.
  - `recheck_cooldown_seconds` integer, required: A camera re-checked within this many seconds is not re-checked again (POST /analytics/camera-health/recheck counts it as already_pending). Published so the UI shows the window the server enforces.
  - `cameras` CameraHealthRow[], required: One row per in-scope camera, sorted needs-attention first, then site name, then camera label.
    - `id` string, uuid, required: Camera row id.
    - `camera_label` string, required: Operator-facing camera name.
    - `camera_id` string, nullable: Hardware camera id.
    - `kvs_stream_name` string, nullable: KVS stream name.
    - `integration` string, nullable: Camera integration.
    - `ingest_mode` string, nullable: 'motion_clip' or 'continuous'. Only motion_clip cameras can produce health snapshots.
    - `ptz` boolean, required: Pan/tilt/zoom flag; when true the 'moved' issue is suppressed.
    - `parent_id` string, uuid, required: Client (parent) id.
    - `parent_name` string, nullable: Client company name.
    - `site_id` string, uuid, required: Site id.
    - `site_name` string, nullable: Site name.
    - `monitoring_active` boolean, required: Whether the camera's schedule has it monitored right now.
    - `connectivity` string, required: 'online', 'degraded', 'offline', 'off_schedule', or 'unknown'.
    - `connectivity_reason` string, nullable: Why, when there is a reason: the pipeline reason, 'never_reported', or the unrecognized pipeline status itself.
    - `connectivity_since` string, date-time, nullable: When the connectivity CLASS last changed. Null until the camera's first telemetry. Does not move on a healthy/filtering flip — both are 'online'.
    - `pipeline_status` string, nullable: Raw telemetry-owned pipeline status the connectivity was derived from.
    - `pipeline_reason` string, nullable: Raw telemetry-owned pipeline reason.
    - `detection_mode` string, nullable: Camera's DeepAlert routing mode: 'legacy' | 'ii_gated'; null means legacy. On an 'ii_gated' camera the object-of-interest gate runs inside the analysis pipeline, so ingestion's own DeepAlert validator signal is not meaningful for it.
    - `ii_deepalert_state` string, nullable: Last observed analysis-pipeline DeepAlert outcome for an 'ii_gated' camera: 'ok' | 'error' | 'timeout'. Null means no observation yet — delegated/unknown, never 'healthy'. Event-driven and sticky: it moves only when a motion event produces an evaluation callback, and the last observation stands until the next one.
    - `ii_deepalert_at` string, date-time, nullable: When the observation behind ii_deepalert_state was made — the creation time of the event it came from, not a receipt time. Null when there has been no observation. Render 'ok' as a DATED observation ('ok as of <this>'), never as an unbounded current-health claim: the value is sticky and only a later event can replace it.
    - `last_frame_at` string, date-time, nullable: Most recent frame the ingest worker reported.
    - `last_motion_at` string, date-time, nullable: Most recent motion the ingest worker reported.
    - `last_pipeline_telemetry_at` string, date-time, nullable: When telemetry was last received for this camera.
    - `health_sample_requested_at` string, date-time, nullable: When an operator last asked this camera for an on-demand health sample (POST /analytics/camera-health/recheck). Null when never requested. A later latest_sample.received_at means a sample arrived after the request, not necessarily that it answered the request: scheduled samples, delayed uploads, and retries can also arrive later. This timestamp does not correlate a sample with a specific request. Only motion_clip cameras can answer.
    - `snapshot_unavailable_reason` string, nullable: Why there is no thumbnail, in precedence order: 'continuous_ingest' (no ring buffer, so no frame is ever extracted), then 'never_sampled' (no sample within retention), then 'no_frame_in_latest_sample' (the camera reported but could not extract a frame), else null.
    - `latest_sample` CameraHealthSample: The camera's most recent sample within the 7-day retention window — NOT restricted to the last 24 hours, so an off-schedule camera keeps showing its last frame with an honest age.
      - `id` string, uuid, required: Sample row id.
      - `captured_at` string, date-time, required: Worker clock at frame capture.
      - `received_at` string, date-time, required: When the API received the sample; compare with captured_at to spot a skewed worker.
      - `snapshot_url` string, nullable: Presigned GET URL for the captured JPEG. Null when the sample has no frame, or when presigning this one row failed (the rest of the response is still served).
      - `snapshot_expires_in` integer, nullable: Seconds snapshot_url stays valid, counted from generated_at. Null when there is no URL.
      - `sharpness` number, nullable: Normalized sharpness 0..1; higher is sharper.
      - `luminance` number, nullable: Mean grayscale luminance 0..1.
      - `occlusion_pct` number, nullable: Share of the frame that is flat and unlike the reference. Null when no reference image is set.
      - `shift_pct` number, nullable: How far the view has moved from the reference. Null when no reference image is set.
      - `identical_run` integer, required: Consecutive samples ending here that share an identical frame hash; 1 when the hash changed, is null, or this is the first point. Derived on each read from the camera's full 24-hour sample set (not from the truncated samples_24h list, so a run is never cut short by the payload cap). A sample with no 24-hour samples behind it — the 7-day fallback — reports 1. Runs are counted over SCHEDULED samples only: an on-demand sample (on_demand=true) is not part of any run and reports 1.
      - `width` integer, nullable: Probed stream width.
      - `height` integer, nullable: Probed stream height.
      - `fps` number, nullable: Probed average frame rate.
      - `bitrate_kbps` integer, nullable: Ingest bitrate of the sampled segment.
      - `reconnects` integer, required: Runner restarts since the previous sample (a delta, not a running total).
      - `on_demand` boolean, required: True when this sample answered an operator's re-check request rather than the hourly schedule. Excluded from the frozen-frame run and from the bitrate baseline; otherwise an ordinary sample.
    - `reference` CameraHealthReference
      - `url` string, required: Presigned GET URL for the camera's reference image.
      - `set_at` string, date-time, required: When the reference image was adopted.
      - `expires_in` integer, required: Seconds url stays valid, counted from generated_at.
    - `samples_24h` CameraHealthSamplePoint[], required: 24-hour sample series, ascending by captured_at, at most 48 points. The newest point is always retained. Over the cap the oldest other on-demand points are dropped first, then the oldest points of any kind.
      - `captured_at` string, date-time, required: Worker clock at frame capture.
      - `bitrate_kbps` integer, nullable: Ingest bitrate, kbps.
      - `sharpness` number, nullable: Normalized sharpness 0..1.
      - `luminance` number, nullable: Mean luminance 0..1.
      - `occlusion_pct` number, nullable: Occlusion vs the reference image.
      - `shift_pct` number, nullable: Shift vs the reference image.
      - `identical_run` integer, required: Consecutive samples ending at this point that share an identical frame hash. Derived on each read from the camera's full 24-hour sample set, so a point at the start of this (truncated) list continues the run it belongs to rather than restarting at 1. Counted over scheduled samples only; an on-demand point reports 1.
      - `reconnects` integer, required: Reconnects in this interval.
      - `on_demand` boolean, required: True when this point is an on-demand re-check sample rather than an hourly one.
    - `reconnects_24h` integer, nullable, required: Reconnect deltas summed over ALL samples received in the last 24 hours, including on-demand points omitted from samples_24h. Null when no samples were received in that window; zero means samples exist and recorded no reconnects.
    - `history_24h` CameraHealthHistoryPoint[], required: Exactly 24 hourly connectivity states, ascending, ending with the current hour.
      - `hour_start` string, date-time, required: Start of the UTC hour this entry covers.
      - `state` string, required: Connectivity for that hour, from the same vocabulary as the row's connectivity field.
    - `uptime_24h_pct` number, nullable: Online+degraded hours over MONITORED hours. Null (not zero) when the camera was off-schedule for all 24 hours.
    - `view_check` CameraHealthViewCheck: The newest AI view check of this camera: a vision model's verdict on one health-sample frame. Checked every few hours, plus soon after an on-demand re-check sample arrives.
      - `checked_at` string, date-time, required: When the verdict was recorded.
      - `captured_at` string, date-time, required: Worker clock at capture of the frame that was judged. Display only: checks are ordered by received_at.
      - `received_at` string, date-time, required: When the API received the judged frame (server clock). Checks are ordered by this, and the 'obstructed' confirm gap is measured on it.
      - `obstructed` boolean, nullable: Whether the model judged the view obstructed. A single positive check does not raise the 'obstructed' issue on its own.
      - `kind` string, nullable: What the model saw: none, object, dirty_lens, web_or_insect, water_or_condensation, glare, out_of_focus, or other. 'glare' never raises the issue.
      - `coverage_pct` integer, nullable: Roughly how much of the frame the obstruction covers, 0-100.
      - `reason` string, nullable: The model's one-sentence explanation, at most 300 characters.
      - `on_demand` boolean, required: True when the judged frame was an on-demand re-check sample.
    - `issues` CameraHealthIssue[], required: Server-derived issues, evaluated against the published thresholds.
      - `key` string, required: Stable issue identifier: frozen, obstructed, blur, occlusion, moved, dark, bright, flapping, bitrate_spike, deepalert_degraded, camera_down. 'obstructed' is the AI view check (confirmed on two checks hours apart); 'occlusion' is the reference-image comparison.
      - `category` string, required: 'image' (needs a captured frame), 'stream' (ingest transport), or 'pipeline' (downstream analytics).
      - `severity` string, required: 'info', 'warn', or 'crit'. Anything above 'info' marks the camera as needing attention.
      - `label` string, required: Short operator-facing title.
      - `detail` string, required: One-sentence explanation including the measured value.
    - `needs_attention` boolean, required: True when connectivity is offline/degraded/unknown, or any issue is above 'info' severity. Drives the default sort.

## 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)
