---
title: "Get Delivery Latency"
method: GET
path: "/analytics/delivery-latency"
tags: ["analytics"]
---

# Get Delivery Latency

`GET /analytics/delivery-latency`

## 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/delivery-latency`
- Auth: HTTP bearer

How long alarms take to reach an operator — the Analytics → System
Health delivery-latency panel.

Owner + support through the SAME dependency /system-health uses, and for the
same reason: every number here is a cross-tenant fleet aggregate, and the
camera rows name cameras, sites and parents under every customer. There is
deliberately no scoped variant — a narrowed one would leak fleet facts by
construction — so there is no parent_id/site_id parameter and nothing for
ensure_analytics_filter_in_scope to check.

`window` IS A TWO-VALUE ENUM, NOT AN `hours` INT. The cost profile is tuned
for exactly these two windows and the ~5-minute cache is keyed on them, so
a free int would make the cache's key space unbounded and would invite
arbitrary scans of the highest-insert table in the system. Anything else is
a 422 rather than an expensive 200.

## Query parameters

- `window` '24h' | '7d': Which window: the latest 24 clock-hour buckets, or the latest 168 (7 days). The last bucket is the hour IN PROGRESS and is partial, so the span is 23 (or 167) whole hours plus the elapsed part of the current hour — the response publishes the exact bounds as window_start/window_end.

## Response `200`

Successful Response

- GetDeliveryLatencyResponse: Owner-only fleet alarm-delivery latency: how long an alarm takes to get from motion at the camera onto an operator's screen, and which cameras are slowest. Nothing here is tenant-scoped — these are cross-tenant fleet aggregates naming cameras, sites and parents under every customer, which is why the route is owner-only with no filter parameters. Served from a ~5-minute per-process cache keyed on the window.
  - `generated_at` string, date-time, required: When this payload was computed. Served from a ~5-minute per-process cache, so two consecutive polls can land on different API tasks and see slightly different values — key any freshness banner off your own fetch time, not off this alone.
  - `cache_age_seconds` integer, required: How old the served payload is, in seconds. 0 means this request computed it.
  - `window_hours` integer, required: How many CLOCK-HOUR BUCKETS the window is: 24 or 168. `hourly` and every `hourly_p50_s` have exactly this many entries. It is a bucket count, NOT the elapsed span — the last bucket is the hour in progress and is partial, so the real span is `window_end - window_start` = 23 (or 167) whole hours plus the elapsed part of the current hour. A '24h' window at 18:05 therefore does NOT include yesterday 18:05-19:00.
  - `window_start` string, date-time, required: Inclusive start of the window, in UTC — the top of the oldest bucket, and the same value as `hourly[0].hour`. Published so a consumer can state the span it was actually given rather than inferring a rolling window from `window_hours`.
  - `window_end` string, date-time, required: Exclusive end of the window, in UTC — `generated_at`, i.e. mid-hour. Truncated to the hour it equals `hourly[-1].hour`, the partial bucket in progress.
  - `fleet` DeliveryLatencyFleet, required: The whole fleet over the selected window.
    - `events` integer, required: Eligible alarms in the window — the population every percentile here is over. Excludes sentinels, clock-suspect rows, rows with no reported motion time, rows not yet visible in the queue, and demo uploads.
    - `cameras` integer, required: Distinct cameras that contributed at least one eligible alarm.
    - `motion_to_queue` DeliveryLatencyPercentiles, required: A p50/p90 pair over one leg of the delivery path, in seconds. BOTH ARE NULLABLE AND NULL IS NOT ZERO. An empty bucket is not "0 s of latency" — it is an hour in which no alarm was delivered, which on a schedule-gated fleet is the normal state of most cameras most of the time. Render null as "—", never as 0s: a 0 would draw a reassuring dip exactly where there is no data.
      - `p50_s` number, nullable: Median, in seconds. Null when the bucket had no eligible alarms.
      - `p90_s` number, nullable: 90th percentile, in seconds. Null when the bucket had no eligible alarms.
    - `motion_to_clip` DeliveryLatencyPercentiles, required: A p50/p90 pair over one leg of the delivery path, in seconds. BOTH ARE NULLABLE AND NULL IS NOT ZERO. An empty bucket is not "0 s of latency" — it is an hour in which no alarm was delivered, which on a schedule-gated fleet is the normal state of most cameras most of the time. Render null as "—", never as 0s: a 0 would draw a reassuring dip exactly where there is no data.
      - `p50_s` number, nullable: Median, in seconds. Null when the bucket had no eligible alarms.
      - `p90_s` number, nullable: 90th percentile, in seconds. Null when the bucket had no eligible alarms.
    - `clip_to_queue` DeliveryLatencyPercentiles, required: A p50/p90 pair over one leg of the delivery path, in seconds. BOTH ARE NULLABLE AND NULL IS NOT ZERO. An empty bucket is not "0 s of latency" — it is an hour in which no alarm was delivered, which on a schedule-gated fleet is the normal state of most cameras most of the time. Render null as "—", never as 0s: a 0 would draw a reassuring dip exactly where there is no data.
      - `p50_s` number, nullable: Median, in seconds. Null when the bucket had no eligible alarms.
      - `p90_s` number, nullable: 90th percentile, in seconds. Null when the bucket had no eligible alarms.
    - `band_7d` DeliveryLatencyBand, required: The rolling 7-day daily baseline the current window is read against, shaped like `SystemHealthDailyBand`. EACH NUMBER IS A MEDIAN OF DAILY VALUES OF THE SAME STATISTIC, so it is comparable to the fleet number it sits under: `p50_s` is the median of the seven daily p50s, `p90_s` the median of the seven daily p90s. It is NOT a p50/p90 over one daily series (round-0 OQ-3, ruling (b)). Built from COMPLETE UTC days only: the band runs from midnight seven days ago to TODAY'S UTC MIDNIGHT, so it is exactly the seven preceding whole days and today's alarms — the thing being read against it — are never in it. `days` is how much history the band actually had.
      - `p50_s` number, nullable: Median of the daily motion-to-queue p50s. Null when no complete day had eligible alarms.
      - `p90_s` number, nullable: Median of the daily motion-to-queue p90s. Null when no complete day had eligible alarms.
      - `days` integer, required: How many of the seven days contributed a motion-to-queue value. Fewer than 7 means a partial baseline (a new deploy, or whole days on which the fleet delivered nothing) — say so rather than drawing a confident envelope.
    - `over_60s` integer, required: Eligible alarms whose MOTION-TO-QUEUE total exceeded 60 seconds (the whole path, not a single leg).
    - `failsafe_sentinels` integer, required: Queue-visible alarms in the window that NO AI verdict ever arrived for, surfaced by the 5-minute fail-safe sweep. Excluded from every percentile — they record that the pipeline did not answer, not how fast it did — so a persistent nonzero value is its own signal.
    - `early_broadcast_share_pct` number, nullable: Share of the eligible alarms that reached the operator BEFORE their AI verdict. Their queue leg is ~0 s by construction, so read the fleet median as a blend of the two populations. Null when the window is empty (an empty window is not 0%).
    - `clock_suspect_cameras` integer, required: Cameras with at least one clock-suspect alarm in the window. A bad clock and a camera-sourced clock are different claims — see `motion_clock` on the camera rows for the second one.
    - `clock_suspect_events` integer: Alarms excluded because their clip leg was under -5 s or over 6 h, i.e. the reported motion time cannot be reconciled with the clip's arrival. Published so the excluded population can be sized rather than left as an unquantified caveat.
    - `missing_occurred_at` integer: Queue-visible alarms excluded because they reported no motion time at all. Read beside `events`: a suspiciously good p50 over a small `events` and a large value here describes a fraction of the fleet's alarms.
  - `hourly` DeliveryLatencyHour[]: Dense hourly series, oldest first, exactly `window_hours` entries.
    - `hour` string, date-time, required: Bucket START, in UTC. Buckets are on `created_at` (the single RDS clock and the same column the window is anchored on), not on the reported motion time.
    - `events` integer, required: Eligible alarms in this hour. 0 for a gap.
    - `p50_s` number, nullable: Median motion-to-queue in this hour, in seconds. Null for an empty hour — not 0.
    - `p90_s` number, nullable: 90th percentile motion-to-queue in this hour, in seconds. Null for an empty hour — not 0.
  - `cameras` DeliveryLatencyCamera[]: Cameras ranked by p90 motion-to-queue, SLOWEST FIRST: at most `top_cameras` rows, and only cameras with at least `min_camera_events` eligible alarms. The TAIL of this list is the fastest AMONG THE RETURNED ROWS, not a second query — it is the fleet's fastest only while the number of qualifying cameras is at or below `top_cameras`, because the cap is applied from the slow end and drops the fastest cameras first. The cap sits far above the monitored fleet, so in practice this is every camera that clears the floor and the tail really is the fleet's fastest. Ties break by eligible-alarm count descending, then camera id ascending, so the order is stable between polls.
    - `camera_id` string, required: Camera id.
    - `camera_label` string, nullable: Operator-facing camera name.
    - `site_id` string, required: Id of the site the camera belongs to. The stable key for filtering or grouping this table by site; `site_name` is the label for the same entity and is not unique across parents.
    - `site_name` string, nullable: Site the camera belongs to.
    - `account_id` string, nullable: Id of the account (the dealer's customer) the site sits under. NULL WHEN THE SITE SITS DIRECTLY UNDER ITS PARENT — accounts are an optional level, so null means 'no account', never 'unknown'.
    - `account_name` string, nullable: Name of that account. Null exactly when `account_id` is null — the two are never independently null, so an account filter may key on either.
    - `parent_id` string, required: Id of the dealer/parent the camera's site belongs to. The stable key for filtering or grouping this table by client; `parent_name` is the label for the same entity.
    - `parent_name` string, nullable: Dealer/parent company the site belongs to.
    - `events` integer, required: Eligible alarms for this camera in the window. Never below the min-events floor the payload publishes in `min_camera_events`.
    - `p50_s` number, nullable: Median motion-to-queue, in seconds.
    - `p90_s` number, nullable: 90th percentile motion-to-queue, in seconds. The sort key for this table.
    - `clip_p50_s` number, nullable: Median clip-acquisition leg, in seconds.
    - `queue_p50_s` number, nullable: Median AI/queue-wait leg, in seconds.
    - `motion_clock` 'ingestor' | 'camera', required: Whose clock stamped this camera's motion times. 'camera' means at least one eligible alarm in the window uses the camera's clock (legacy rtsp_motion); 'ingestor' means the ingestion host stamped them (server_motion or ONVIF rtsp_motion_receipt). Mixed windows remain 'camera' until the eligible legacy rows age out. Derived per event from events_metadata.source, falling back to the camera's current ingest mode for missing or unrecognized sources.
    - `over_60s` integer, required: This camera's eligible alarms whose motion-to-queue total exceeded 60 seconds.
    - `clock_suspect_events` integer, required: This camera's alarms excluded as clock-suspect. Not included in `events`.
    - `hourly_p50_s` number[]: This camera's hourly median motion-to-queue across the window, oldest first — exactly `window_hours` entries, for a sparkline. ELEMENTS ARE NULLABLE: an hour with no alarm is null, not 0, and the sparkline must draw a gap rather than a dip to the floor.
  - `min_camera_events` integer, required: Minimum eligible alarms for a camera to appear in `cameras` at all. Published so the UI labels the rule the server actually applied instead of restating a constant that can drift.
  - `top_cameras` integer, required: Maximum number of camera rows returned. A BOUND, NOT A PAGE SIZE — there is no second page, and the value is deliberately set well above the monitored fleet so `cameras` is in practice the complete ranked list. Published, like `min_camera_events`, so the UI labels the rule the server actually applied instead of restating a constant that can drift.
  - `notes` string[]: Server-authored caveats, with this window's counts already interpolated. Render them verbatim wherever these numbers are shown — several of them are the difference between reading the median correctly and misreading it.

## 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/00252d8227c1?raw)
