---
title: "List traffic events"
method: GET
path: "/events"
tags: ["events"]
---

# List traffic events

`GET /events`

Paginated list of traffic events with filters. Free plan requires
jurisdiction parameter.

**Ordering** (not configurable): severity first — `critical`, `major`,
`moderate`, then everything else — and within a severity band by
`last_updated` descending, tie-broken by `id` descending. Because
`last_updated` is a content-change time, the top of a band is what
changed most recently, not what we polled most recently.

The `id` tiebreak matters for pagination: `last_updated` alone is not a
total order (events land in batches that share a timestamp), so without
it two pages could repeat or skip rows.

**Batch lookup**: pass `?ids=id1,id2,...` to fetch a known set of
events by id. When `ids` is set, all other filters except plan
data delay are ignored and pagination is disabled — the response
always returns the full set in one page. Archived events are
filtered per plan: non-analytics plans
(`allow_analytics: false`) receive only events whose effective
start_time is within the last 7 days. Active events are always
returned regardless of age. `total` reflects the post-filter
count. The batch size is capped by the plan's `max_batch_size`.

## Query parameters

- `ids` string
- `jurisdiction` string
- `limit` integer
- `offset` integer
- `bbox` string
- `lat` number
- `lng` number
- `radius_km` number
- `type` string
- `sub_type` string
- `cause` 'accident' | 'disabled_vehicle' | 'debris' | 'spill' | 'fire' | 'police_activity' | 'animal' | 'congestion' | 'hazard'
- `severity` 'minor' | 'moderate' | 'major' | 'critical'
- `status` 'active' | 'archived' | 'all'
- `road` string
- `start_time_from` string, date-time
- `start_time_to` string, date-time

## Response `200`

Paginated events

- PagedEvents
  - `attribution` SourceCredit[] — Required data-source credits for the sources present in `data`. Present only for sources whose license mandates a displayed credit (e.g. CC-BY, OGL). See `GET /data-sources` for the full catalogue.
    - `attribution` string — The exact credit string this source's license requires you to display.
    - `license` string
    - `license_url` string, uri
    - `source_code` string — Jurisdiction or program code (e.g. `ESP`, `OSM`, `EIA`).
    - `source_name` string
  - `data` TrafficEvent[]
    - `affected_roads` string[]
    - `archive_reason` 'observed' | 'stale_sweep' | 'expired' | 'null', nullable — How the event was archived. NULL while `status='active'`; set alongside `archived_at` at archival time. - `observed` — upstream returned a clean response and the event was not in it, so the per-poll archival path retired it. This is the normal case. - `stale_sweep` — the per-poll path could not retire the event because upstream responses were empty or errored (so we cannot prove the event is gone vs. a transient outage). After roughly `poll_interval × sweep_factor` with no fresh observation (default factor = 15), a safety-net sweep forces the event to archived. A persistently high `stale_sweep` ratio on a source signals that the upstream feed often fails to emit clearance records or is unreliable in general — treat the `archived_at` for such events as "presumed gone by", not a precise end timestamp. - `expired` — the event outlived its own `end_time` by more than the grace period (90 days) while the upstream feed was still publishing it as current. Some feeds are registries that never retire a record, so neither path above can ever fire on them: without this, work orders whose validity window closed years ago would be served as active indefinitely. Because the verdict is derived from the record's own `end_time` on every poll, such an event returns to `active` automatically if upstream later moves the end date forward. Treat `archived_at` as "first poll at which the event was more than 90 days past its stated end", not as an observed end — the real end is `end_time`.
    - `archived_at` string, date-time, nullable — Set the moment our sweep first observed the event missing from the upstream feed.
    - `cause` 'accident' | 'disabled_vehicle' | 'debris' | 'spill' | 'fire' | 'police_activity' | 'animal' | 'congestion' | 'hazard' — Optional underlying operational cause, classified independently of `type` (unlike `sub_type`, which is scoped under `type`). A crash reported by the source as a closure has `type=closure`, `sub_type=lane_closure`, and `cause=accident` — so filtering `cause=accident` catches accident-caused events regardless of how the source modeled them. Omitted when no cause could be detected. Additive: never replaces `type`, `sub_type`, or `severity`.
    - `created_at` string, date-time — When we first stored this event.
    - `description` string
    - `direction` string, nullable
    - `effective_end_time` string, date-time, nullable — Best available end time for the event. Equals `end_time` when the upstream feed reported one; otherwise falls back to `archived_at` (the moment our sweep first observed the event had disappeared from the feed). NULL only for active events with no upstream-reported end — i.e., events that are still ongoing. Use this field for "incident duration" analytics.
    - `end_time` string, date-time, nullable — Upstream-reported end time. NULL for events the upstream feed never assigned an explicit end (the common case — most 511 feeds simply drop resolved incidents rather than emitting a final timestamp).
    - `estimated_end_time` string, date-time, nullable
    - `estimated_start_time` string, date-time, nullable
    - `id` string
    - `jurisdiction` string
    - `lanes_affected` string, nullable
    - `last_updated` string, date-time — When this event's **content** last changed in our data — not when we last polled it. Re-observing an unchanged event does not move it, so a long-running roadworks entry can legitimately carry a `last_updated` that is months old while still being live and confirmed on every poll. Use it to answer "what changed since I last synced", and do not read it as a freshness or liveness signal. The comparison covers the fields we publish (type, sub_type, cause, status, severity, title, description, location, affected_roads, direction, lanes_affected, end_time, the estimated_* times and road_class); it is byte-level, so a re-worded description counts as a change. `/events` and `/events/geojson` sort by severity first and `last_updated` descending second (see the endpoint descriptions). `Feature.last_updated` means the same thing as of 2026-07-23; it used to be poll time.
    - `latitude` number
    - `location` object — GeoJSON geometry (Point, LineString, or Polygon) describing where the event applies.
    - `longitude` number
    - `metadata` object — Raw upstream attributes preserved as opaque JSON. Shape varies by source.
    - `road_class` 'interstate' | 'us_highway' | 'state_highway' | 'local'
    - `severity` 'minor' | 'moderate' | 'major' | 'critical'
    - `source` string — Jurisdiction code
    - `source_created_at` string, date-time, nullable — Upstream-reported creation timestamp, when available.
    - `source_id` string
    - `source_updated_at` string, date-time, nullable — Upstream-reported last-modified timestamp, when available.
    - `start_time` string, date-time
    - `status` 'active' | 'archived'
    - `sub_type` 'accident' | 'disabled_vehicle' | 'debris' | 'hazard' | 'congestion' | 'police_activity' | 'fire' | 'spill' | 'animal' | 'roadwork' | 'maintenance' | 'bridge_work' | 'utility_work' | 'full_closure' | 'ramp_closure' | 'lane_closure' | 'bridge_closure' | 'seasonal_closure' | 'ice' | 'snow' | 'flooding' | 'high_wind' | 'fog' | 'avalanche' | 'weight' | 'height' | 'width' | 'length' | 'hazmat' | 'chain_control' | 'icy' | 'snow_covered' | 'wet' | 'slush' | 'dry' | 'sporting' | 'parade' | 'concert' — Optional additive second-level classification under `type` (e.g. `type=incident`, `sub_type=accident`). Omitted when the event could not be classified. Never replaces `type` — existing `type` filters are unaffected.
    - `title` string
    - `type` 'incident' | 'construction' | 'closure' | 'special_event' | 'weather' | 'road_condition' | 'planned' | 'hazard'
  - `has_more` boolean
  - `limit` integer
  - `offset` integer
  - `total` integer

## Other responses

- `403` — Plan limit exceeded. Common causes: jurisdiction required on Free, trial expired, `start_time_from`/`start_time_to` reaches beyond 7 days without analytics access, `status=archived` or `status=all` requested without a `start_time_from` inside the 7-day window.

---

[API](https://skmtc.net/road511/apis/road511-traffic-api.md) · [All operations](https://skmtc.net/road511/apis/road511-traffic-api/llms.txt) · [OpenAPI document](https://skmtc-service-staging.skmtc.workers.dev/v1/apis/road511/road511-traffic-api/versions/39a088b9c155/schema)
