---
title: "Get Event History"
method: GET
path: "/events/history"
tags: ["events"]
---

# Get Event History

`GET /events/history`

## 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/events/history`
- Auth: HTTP bearer

Reviewed-or-expired alarms, tenant-scoped, paginated and filterable.

All six filter params are optional and additive: absent means today's
unfiltered behaviour, and `total` is the FILTERED count. The semantics
(the half-open window on the effective sort column, the expired clamp on
the evaluation-only filters, the q field set) live in
EventsService.history_filter_conditions.

## Query parameters

- `state` 'dismissed' | 'archived' | 'expired'
- `limit` integer
- `offset` integer
- `order` 'desc' | 'asc'
- `sort` 'alarm_time' | 'reviewed_at' | 'closed_at': alarm_time: alarm creation; reviewed_at: disposition time (clamped to alarm_time for expired rows); closed_at: disposition time for dismissed/archived rows or expiry time for expired rows. The date window uses this same timestamp.
- `from` string, nullable: Inclusive start of the window, an ISO-8601 datetime WITH a UTC offset (a naive value, a bare date, or an epoch number is a 422). Applied to the row's EFFECTIVE sort timestamp: the alarm time under sort=alarm_time, the review time under sort=reviewed_at, or the disposition/expiry time under sort=closed_at. May be sent without `to`.
- `to` string, nullable: EXCLUSIVE end of the window — [from, to) — an ISO-8601 datetime WITH a UTC offset, same rejections as `from`. Must be after `from` when both are given. May be sent without `from`.
- `disposition` string[], nullable: Repeatable. OperatorDisposition MEMBER NAMES (FALSE_ALARM, ESCALATE_DISPATCH, ESCALATE_VOICE_DOWN, SUSPICIOUS_MONITOR, NOT_MONITORED), not the display labels; an unknown name is a 422. Evaluated states only — silently ignored for state=expired, which has no evaluation to filter on.
- `actor` 'operator' | 'agent', nullable: Who disposed the alarm: a human operator or the agent. Evaluated states only — silently ignored for state=expired.
- `q` string, nullable: Keyword search, 120 characters or fewer after stripping (longer is a 422; empty or whitespace-only reads as absent). Whitespace splits it into terms and EVERY term must match; a term matches as a case-insensitive substring of the operator/agent notes (evaluated states only), the alarm summary, the zone, the camera label or id, the client name, the site name, or the AI rationale — plus, when the term carries 3 or more digits, the client's contact phone digits. A search with no `from` covers only the 14 days before `to` (or now). This search-only limit does not apply to unfiltered History. Clients should display the effective search window; send `from` to search further back.
- `site_id` string, uuid, nullable: Only this site's alarms (a site UUID; anything else is a 422). Narrows inside the caller's tenant scope: a site the caller cannot see returns an empty page.
- `audit` 'needs' | 'audited' | 'overturned' | 'disputed' | 'pending_review', nullable: Call-audit lens, for owners, monitoring managers and operators (read-only and customer roles get a 403). A lead sees every audit; an operator's lenses exclude their own calls and only match audits they are party to. needs: sampled or rushed human calls with no audit yet (for a lead also calls whose PEER audit is still open to a lead: a peer uphold or a pending peer overturn), limited to calls inside the 30-day clip window or with a recorded retained clip copy (an age rule, not an S3 check); audited: has an audit; overturned: the audit overturned the call; disputed: a dispute awaits a ruling; pending_review (LEADS ONLY, 403 otherwise): peer overturns awaiting lead confirmation. Evaluated states only — silently ignored for state=expired.
- `operator_id` string, uuid, nullable: Only alarms this operator decided (their evaluation's operator). Refused for curated customer roles. Evaluated states only — silently ignored for state=expired.
- `include_total` boolean: false skips the filtered count and returns total: null. For background refreshes that already have a count from the opening fetch; counting the whole filtered history on every poll was most of the database load in the 2026-10-13 incident. With audit=needs on the dismissed or archived state the count is best effort: past its own 2 s budget total is null and the rows are still returned.

## Response `200`

Successful Response

- GetEventHistoryResponse
  - `items` object[], required: Page of history rows, newest first. Each row is {state, event, camera, site, site_name, parent, operator_evaluation, thread, interpretation_result, poster_url, operator_name, audit, audit_reasons}. site contains the site id, name and the IANA timezone for site-local pipeline timestamps; site_name is retained for existing clients. interpretation_result carries the same to_dict_frontend shape the active queue and GET /events/{id} use (incl. terminal_stage and gate_outcome), and is null for II-off cameras and rows aged into history before any eval landed. The camera dict omits the resolved rtsp_url on this list path. poster_url is a presigned URL to the alert-frame JPEG derived from the event's clip key (null when there is no clip, the clip key doesn't match the expected shape, or presigning failed) — the object may not exist yet for pre-cutover rows, so the URL can 404 client-side by design. event.queued_at is additive and optional: when the row entered the operator queue, server-side (derived, never stored — the early-broadcast stamp, else the AI verdict's time, else creation time for a row queued at ingest), and null for a row that never became queue-visible; it is the same value and shape the queue snapshot and GET /events/{id} carry. operator_name (the deciding operator's First Last, central-staff callers only, human rows only; an audited AI-agent close carries "Verdun Agent" from the audit snapshot), audit (the CallAuditOut call audit: leads see every audit, anyone else only on a call they made) and audit_reasons (sample and/or rushed: why the call is up for audit; for the AI agent's closes only 'sample'; owners, monitoring managers and operators) are additive and null / [] when not applicable.
  - `total` integer, nullable, required: Total rows matching the state filter AND every optional filter on the request (from/to, disposition, actor, q, site_id, audit, operator_id) — the filtered count, not the state total, so the client's result line can read it directly. null when the request sent include_total=false (the console's background poll keeps the count from its opening fetch), and for audit=needs on the dismissed or archived state when its count runs past its own 2 s budget (the rows are still returned).

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