{
  "components": {
    "parameters": {
      "bbox": {
        "description": "Bounding box: minLng,minLat,maxLng,maxLat",
        "in": "query",
        "name": "bbox",
        "schema": {
          "type": "string"
        }
      },
      "cursor": {
        "description": "Keyset pagination: return the page immediately after this position.\nPass the `next_cursor` from the previous response; omit it for the first\npage. Mutually exclusive with `offset` (sending both is a 400).\n\nPrefer this over `offset` for any full-set read. It is not subject to\nthe offset cap at all, and unlike `offset` its cost is flat regardless\nof how deep you are — a page at position 160,000 costs the same as the\nfirst one (measured: 2.9 ms). It is also the migration path for the\noffset cap drop on 2026-10-06 described under `offset`.\n\nThe value is opaque and its format is not part of this contract: treat\nit as a token to echo back, do not construct or parse it. A cursor stays\nvalid across requests; rows added after you started paging carry a newer\n`last_updated` and so sort ahead of your position, meaning a walk sees a\nconsistent snapshot moving backwards in time rather than shifting under\nyou the way `offset` does.\n",
        "in": "query",
        "name": "cursor",
        "schema": {
          "type": "string"
        }
      },
      "ends_after": {
        "description": "Only features scheduled to end on or after this instant. Accepts an\nRFC3339 timestamp or a bare `YYYY-MM-DD` date (UTC midnight).\n\nMatches against `estimated_end_time`, falling back to `end_time`. A\nfeature with a known start and no end is treated as open-ended and\nalways matches. A feature with no dates at all never matches.\n",
        "in": "query",
        "name": "ends_after",
        "schema": {
          "example": "2026-08-01",
          "type": "string"
        }
      },
      "ends_before": {
        "description": "Only features scheduled to end on or before this instant. Accepts an\nRFC3339 timestamp or a bare `YYYY-MM-DD` date (UTC midnight).\n\nOpen-ended features (known start, no end) never match — they are not\nprovably finished by any given date.\n",
        "in": "query",
        "name": "ends_before",
        "schema": {
          "example": "2026-12-31",
          "type": "string"
        }
      },
      "jurisdiction": {
        "description": "Filter by jurisdiction code (e.g., `GA`, `ON`, `CA`). Required on Free plan.\n\n**Auto-expansion:** querying a state's primary code that has WZDx/CWZ\nor sub-state regional siblings (see `GET /jurisdictions/groups`) is\nexpanded transparently to include all members. For example,\n`?jurisdiction=CA` returns rows from `CA`, `WZDX_CA`, and `511SF`.\n\nTo opt out, query a non-primary code directly (`?jurisdiction=WZDX_CA`\nreturns WZDx-only rows) or pass a comma-separated list explicitly\n(`?jurisdiction=CA,WZDX_CA`), which bypasses expansion and matches\neach code literally.\n",
        "in": "query",
        "name": "jurisdiction",
        "schema": {
          "type": "string"
        }
      },
      "lat": {
        "description": "Latitude for radius search",
        "in": "query",
        "name": "lat",
        "schema": {
          "type": "number"
        }
      },
      "limit": {
        "description": "Results per page (capped by plan)",
        "in": "query",
        "name": "limit",
        "schema": {
          "default": 100,
          "type": "integer"
        }
      },
      "lng": {
        "description": "Longitude for radius search",
        "in": "query",
        "name": "lng",
        "schema": {
          "type": "number"
        }
      },
      "offset": {
        "description": "Pagination offset, for jumping a short way into a result set. Currently\ncapped at **50,000**; past that the request is rejected with 400,\nbecause skipping N rows costs time proportional to N — measured on\nproduction, one page of 100: offset 0 = 8 ms, 5,000 = 557 ms, 10,000 =\n698 ms, 50,000 = 3,028 ms.\n\n⚠️ **Planned change: on 2026-10-06 this cap drops from 50,000 to\n10,000.** If you page deeper than 10,000, move those loops to `cursor`\nbefore then. Requests with an offset at or below 10,000 are unaffected,\nas is any shallow or \"jump to page N\" use.\n\nFor anything but a shallow jump, prefer `cursor`. It has no ceiling, its\ncost does not grow with depth (~3 ms at any position), and it is the\nonly way to read a set larger than the cap.\n",
        "in": "query",
        "name": "offset",
        "schema": {
          "default": 0,
          "maximum": 50000,
          "type": "integer"
        }
      },
      "radius_km": {
        "description": "Radius in km (used with lat/lng).\n\n**Capped at 200 when combined with `group`.** A group expands to many feature types, and a wide radius over all of them cannot be served inside the request budget, so the combination returns 400 rather than timing out. Lower the radius, or query a single `type` — single-type radius searches have no limit.",
        "in": "query",
        "name": "radius_km",
        "schema": {
          "default": 50,
          "type": "number"
        }
      },
      "starts_after": {
        "description": "Only features scheduled to start on or after this instant. Accepts an\nRFC3339 timestamp or a bare `YYYY-MM-DD` date (UTC midnight).\n\nMatches against `estimated_start_time` where the source publishes a\nplanned date, falling back to `start_time` otherwise. Features with no\nknown start are excluded.\n",
        "in": "query",
        "name": "starts_after",
        "schema": {
          "example": "2026-08-01",
          "type": "string"
        }
      },
      "starts_before": {
        "description": "Only features scheduled to start on or before this instant. Accepts an\nRFC3339 timestamp or a bare `YYYY-MM-DD` date (UTC midnight).\n\nCombine with `ends_after` to select everything whose scheduled window\noverlaps a period: `?starts_before=2026-09-01\u0026ends_after=2026-08-01`\nreturns every project active at some point during August 2026.\n",
        "in": "query",
        "name": "starts_before",
        "schema": {
          "example": "2026-09-01",
          "type": "string"
        }
      }
    },
    "schemas": {
      "APIKey": {
        "properties": {
          "allowed_origins": {
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "created_at": {
            "format": "date-time",
            "type": "string"
          },
          "expires_at": {
            "format": "date-time",
            "type": "string"
          },
          "id": {
            "type": "integer"
          },
          "is_active": {
            "type": "boolean"
          },
          "key_prefix": {
            "type": "string"
          },
          "last_used_at": {
            "format": "date-time",
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "owner_email": {
            "type": "string"
          },
          "rate_limit_rpm": {
            "type": "integer"
          }
        },
        "type": "object"
      },
      "APIWarning": {
        "description": "A non-fatal note about the request, returned alongside a 200. It exists because the problem it reports is otherwise invisible: asking for a jurisdiction code we do not have returned exactly the same empty list as asking for a real one with no data.",
        "properties": {
          "code": {
            "description": "Machine-readable warning kind.",
            "enum": [
              "unknown_jurisdiction",
              "unknown_type",
              "unknown_severity",
              "unknown_status",
              "not_covered",
              "covered_but_empty"
            ],
            "example": "unknown_jurisdiction",
            "type": "string"
          },
          "did_you_mean": {
            "description": "The codes the input most likely meant, best first. The catalogue is keyed on ISO 3166-1 alpha-3 while many callers send alpha-2, so an alpha-2 value resolves to the codes of that country. Absent when nothing plausible resolves.",
            "example": [
              "FRA"
            ],
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "message": {
            "example": "jurisdiction \"FR\" does not exist — did you mean FRA? GET /api/v1/jurisdictions lists every valid code.",
            "type": "string"
          },
          "value": {
            "description": "The offending input, uppercased and trimmed.",
            "example": "FR",
            "type": "string"
          }
        },
        "type": "object"
      },
      "CoverageEntry": {
        "description": "What we hold for one (jurisdiction, type) pair. Three states matter and all three are distinguishable: the entry being absent means we have never held the pair; `row_count` above zero means we hold data now; and `row_count` of zero with a `last_nonzero_at` means we cover it and it is empty at the moment.",
        "properties": {
          "last_nonzero_at": {
            "description": "Last rebuild at which this pair held anything.",
            "format": "date-time",
            "type": "string"
          },
          "latest_data_at": {
            "description": "Newest `last_updated` among the counted rows. A high count with an old timestamp is a stopped feed, which the count alone reports as healthy.",
            "format": "date-time",
            "type": "string"
          },
          "row_count": {
            "example": 1820,
            "format": "int64",
            "type": "integer"
          },
          "type": {
            "example": "cameras",
            "type": "string"
          }
        },
        "type": "object"
      },
      "Customer": {
        "properties": {
          "company": {
            "type": "string"
          },
          "created_at": {
            "format": "date-time",
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "email_verified": {
            "type": "boolean"
          },
          "id": {
            "type": "integer"
          },
          "is_active": {
            "type": "boolean"
          },
          "name": {
            "type": "string"
          },
          "plan_id": {
            "type": "integer"
          },
          "subscription_ends_at": {
            "format": "date-time",
            "type": "string"
          },
          "subscription_status": {
            "enum": [
              "none",
              "active",
              "canceled",
              "past_due",
              "paused"
            ],
            "type": "string"
          },
          "trial_ends_at": {
            "format": "date-time",
            "type": "string"
          }
        },
        "type": "object"
      },
      "Error": {
        "description": "Standard error body. `error` is always present and is the human-readable message — it is stable and safe to display.\n\nA **403 refused by a plan gate** carries additional fields so an integration can react without string-matching the message. They are absent on other errors, so treat them as optional.",
        "example": {
          "code": "plan_jurisdiction",
          "docs_url": "/reference.html",
          "error": "add a jurisdiction filter to this request, e.g. ?jurisdiction=MN (up to 1 per request, comma-separated). The Free plan scopes queries to specific jurisdictions; a higher plan can query all jurisdictions at once.",
          "plan": "free",
          "plan_name": "Free"
        },
        "properties": {
          "code": {
            "description": "Stable machine-readable reason, present on plan-gate 403s. Branch on this rather than on `error`. Values: `plan_jurisdiction` (the request needs a jurisdiction filter, or asked for more jurisdictions than the plan allows), `plan_batch_size`, `plan_event_time_range`, `plan_event_status`, `plan_archived_event`, `plan_inactive_features`, `plan_geojson`, `plan_analytics`, `plan_truck`, `trial_expired`, `subscription_expired`.",
            "example": "plan_jurisdiction",
            "type": "string"
          },
          "docs_url": {
            "description": "Relative, same-host link to the API reference.",
            "example": "/reference.html",
            "type": "string"
          },
          "error": {
            "description": "Human-readable explanation. Always present.",
            "type": "string"
          },
          "plan": {
            "description": "Code of the plan in force when the request was refused.",
            "example": "free",
            "type": "string"
          },
          "plan_name": {
            "description": "Display name of that plan.",
            "example": "Free",
            "type": "string"
          },
          "upgrade_url": {
            "description": "Where to change the plan. Omitted if the portal URL is not configured.",
            "type": "string"
          }
        },
        "required": [
          "error"
        ],
        "type": "object"
      },
      "EventWeatherSnapshot": {
        "properties": {
          "created_at": {
            "format": "date-time",
            "type": "string"
          },
          "distance_km": {
            "description": "Distance from event to weather station in km",
            "type": "number"
          },
          "event_id": {
            "type": "string"
          },
          "humidity": {
            "description": "Relative humidity percentage",
            "type": "number"
          },
          "id": {
            "type": "integer"
          },
          "precipitation": {
            "description": "Precipitation type (none, rain, snow, ice, etc.)",
            "type": "string"
          },
          "reading_recorded_at": {
            "description": "When the weather reading was taken",
            "format": "date-time",
            "type": "string"
          },
          "road_surface": {
            "description": "Road surface condition (dry, wet, icy, etc.)",
            "type": "string"
          },
          "temperature": {
            "description": "Temperature in the unit the reporting agency publishes — Fahrenheit\nfor most US feeds, Celsius for British Columbia and California.\nUse `temperature_c` for arithmetic or cross-jurisdiction comparison.\n",
            "type": "number"
          },
          "temperature_c": {
            "description": "Temperature normalized to Celsius. Omitted when the source feed's\nunit has not been established, so a missing value means \"unknown\",\nnever zero.\n",
            "type": "number"
          },
          "visibility": {
            "description": "Visibility in the unit the source publishes (miles for US feeds).",
            "type": "number"
          },
          "wind_direction": {
            "type": "string"
          },
          "wind_speed": {
            "description": "Wind speed in the unit the source publishes (mph for US feeds).",
            "type": "number"
          }
        },
        "type": "object"
      },
      "Feature": {
        "properties": {
          "description": {
            "type": "string"
          },
          "direction": {
            "type": [
              "string",
              "null"
            ]
          },
          "end_time": {
            "format": "date-time",
            "type": [
              "string",
              "null"
            ]
          },
          "estimated_end_time": {
            "format": "date-time",
            "type": [
              "string",
              "null"
            ]
          },
          "estimated_start_time": {
            "format": "date-time",
            "type": [
              "string",
              "null"
            ]
          },
          "feature_type": {
            "type": "string"
          },
          "geometry": {
            "description": "Full GeoJSON geometry when the feature is a line or polygon; NULL for point-only features (read latitude/longitude instead).",
            "type": [
              "object",
              "null"
            ]
          },
          "has_details": {
            "description": "`true` when this feature's `(source, feature_type)` pair has a\nregistered lazy-load detail function — calling\n`GET /features/{id}/details` or batching via\n`POST /features/details/batch` will return strictly richer data\nthan what's in the list row (multi-view camera URLs, EV connector\navailability, sign message text, etc.). `false` means the list\nrow already carries everything the source publishes, and the\ndetail call would be a no-op.\nNot set on the `/features/geojson` path (that JSON is built in\nPostgres for performance — call `/features` for the hint).\n",
            "type": "boolean"
          },
          "id": {
            "type": "string"
          },
          "is_active": {
            "type": "boolean"
          },
          "jurisdiction": {
            "type": "string"
          },
          "last_updated": {
            "description": "When this feature's **content** last changed in our data — not when\nwe last polled it. Re-observing an unchanged feature does not move\nit, so a camera or a truck restriction confirmed on every poll can\nlegitimately carry a `last_updated` that is weeks old. Use it to\nanswer \"what changed since I last synced\", and do not read it as a\nfreshness or liveness signal.\n\nThe comparison covers the fields we publish (name, description,\nis_active, the start/end/estimated_* times, `properties`, the\ngeometry and the coordinates); it is value-level, so a re-worded\ndescription counts as a change.\n\nIt is NOT the upstream's own timestamp. Where a source publishes one\nand we retain it, it appears inside `properties`.\n\nChanged 2026-07-23: this field used to be poll time, stamped on\nevery refresh. It now matches `TrafficEvent.last_updated`, which has\nalways meant content-change time — the two schemas previously\ndisagreed on what the name meant, and now they do not.\n",
            "format": "date-time",
            "type": "string"
          },
          "latitude": {
            "type": "number"
          },
          "longitude": {
            "type": "number"
          },
          "name": {
            "type": "string"
          },
          "properties": {
            "description": "Type-specific fields (JSONB). The keys vary by `feature_type` and by\nwhat the upstream feed publishes — treat any individual key as\noptional and absent unless documented below.\n\n**EV charging — live availability.** Some `ev_charging` features\ncarry live connector counts alongside the static registry data. The\nkey set is identical across every source that publishes it, so one\nparser handles all of them:\n\n| Key | Type | Meaning |\n|-----|------|---------|\n| `availability_status` | string | `available` · `full` · `out_of_service` · `unknown` — the site rollup |\n| `available_points` | integer | connectors free now |\n| `occupied_points` | integer | connectors in use |\n| `unavailable_points` | integer | connectors faulted or out of service |\n| `reported_points` | integer | connectors the feed actually reported on; equals the length of `refill_points` |\n| `excluded_points` | integer | connectors reported as REMOVED or PLANNED. Present only when non-zero; excluded from the four counts above, so `reported_points` can be lower than a static `ports` count |\n| `is_available` | boolean | shorthand for `available_points \u003e 0` |\n| `availability_source` | string | which upstream published it — e.g. `nl_ocpi`, `fr_irve_dynamique`, `ch_oicp`, `afir_dynamic` |\n| `availability_updated_at` | string | the operator's own observation time, when the feed states one |\n| `refill_points` | array | per-connector detail, when the feed publishes it |\n| `opening_status` | string | site opening state, when the feed publishes it |\n\n`availability_status: \"unknown\"` means the feed reported the site but\nno connector carried a usable status — it is **not** the same as the\nkeys being absent, which means this source publishes no availability\nat all.\n\n⚠️ **Coverage is partial and regional.** Measured 2026-09-07: on the\nEU deployment 136,583 of 511,571 `ev_charging` features carry these\nkeys (about 27%), from four sources; on the North American\ndeployment, none do — every NA row is a static registry. Do not\ntreat a missing key as \"zero free connectors\"; treat it as \"this\nsite does not report availability\".\n\n**Parking and rest areas — occupancy.** `capacity` and\n`available_spots` follow the same rule, and it is worth stating\nplainly because the difference is a safety one: an **absent** key\nmeans the feed publishes no occupancy for that site, while\n`available_spots: 0` is a real count meaning known full. Never read\nan absent key as zero.\n\n⚠️ Until 2026-09-07 four CARS states served `null` occupancy as `0`,\nso 143 rest areas in IA, CO, NE and KS were published as having no\nspaces and none free. The keys are now omitted for those sites.\nKansas is the useful shape to test against: 22 of its 44 rest areas\npublish a real capacity and 22 publish none, so both states of this\nrule appear in one jurisdiction.\n\n**Weather stations — readings.** `temperature`, `wind_speed`,\n`visibility` and `humidity` are omitted, never zeroed, when the feed\npublished no usable value. `temperature` is verbatim in the unit the\nagency publishes and `temperature_c` is the normalized companion;\n`wind_speed` and `visibility` are likewise in the source's own unit,\nwith `wind_speed_units` / `visibility_units` alongside where the feed\nstates one. Where a source publishes a rendered string rather than a\nnumber — Alaska's `\"54 °F\"`, Wyoming's `\"\u003e 1/2 mile (800 m)\"`,\nManitoba's forecast `\"Hi 14 °C\"` — the original is kept in a\ncompanion key (`air_temperature_f`, `relative_humidity_pct`,\n`wind_speed_text`, `visibility_text`, `forecast_temperature`) and the\nnumeric key is present only when a number could actually be read.\n",
            "type": "object"
          },
          "road_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "source": {
            "type": "string"
          },
          "source_id": {
            "description": "Upstream-provided identifier, before our synthetic `id` prefix.",
            "type": "string"
          },
          "start_time": {
            "description": "Populated for time-bounded feature types (future_construction, alerts, special_events, truck_restrictions, etc.). NULL for permanent features.",
            "format": "date-time",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "type": "object"
      },
      "HealthResponse": {
        "properties": {
          "db": {
            "description": "Combined Postgres and Redis liveness, formatted as\n`\u003cpostgres\u003e/\u003credis\u003e` where each segment is `ok` or `down`\n(e.g. `ok/ok`, `ok/down`). Probe timeout is 2 seconds; the\nunderlying error is logged server-side, not returned.\n",
            "example": "ok/ok",
            "type": "string"
          },
          "status": {
            "enum": [
              "ok",
              "degraded"
            ],
            "type": "string"
          },
          "uptime": {
            "type": "string"
          },
          "version": {
            "type": "string"
          }
        },
        "required": [
          "status",
          "version",
          "uptime",
          "db"
        ],
        "type": "object"
      },
      "Jurisdiction": {
        "properties": {
          "code": {
            "description": "Identifier used everywhere else in the API (`source`, `jurisdiction`,\nwebhook filters, etc.). Two-letter postal codes for US states and\nCanadian provinces; `WZDX_\u003cSTATE\u003e` for the parallel WZDx feed; ad-hoc\ncodes for sub-state and federal sources (`511SF`, `EIA`, `FHWA`,\n`WZDX_NPS`, ...).\n",
            "type": "string"
          },
          "country": {
            "description": "ISO 3166-1 alpha-2 (`US`, `CA`, `DE`, ...).",
            "type": "string"
          },
          "group_id": {
            "description": "When non-null, points at a record in `GET /jurisdictions/groups` —\nthe canonical bundle that includes this row's primary code plus its\nWZDx / CWZ / regional siblings. Querying `?jurisdiction=\u003cprimary\u003e`\non `/events` or `/features` auto-expands to all members.\n",
            "type": [
              "string",
              "null"
            ]
          },
          "is_active": {
            "type": "boolean"
          },
          "last_sync_at": {
            "format": "date-time",
            "type": [
              "string",
              "null"
            ]
          },
          "name": {
            "type": "string"
          },
          "official_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "scope": {
            "description": "`state` is the default — real geographic jurisdictions (US states,\nCanadian provinces). `federal` covers cross-cutting national sources\n(`EIA`, `FHWA`, `WZDX_NPS`). `regional` covers sub-state feeds\n(`511SF`, `WZDX_AUSTIN`, `WZDX_MCDOT`, `WZDX_STC_MO`). The default\nresponse from `GET /jurisdictions` is `scope=state` only.\n",
            "enum": [
              "state",
              "federal",
              "regional"
            ],
            "type": "string"
          }
        },
        "required": [
          "code",
          "name",
          "country",
          "scope",
          "is_active"
        ],
        "type": "object"
      },
      "JurisdictionGroup": {
        "properties": {
          "description": {
            "type": "string"
          },
          "id": {
            "description": "Stable slug, e.g. `us-ca`, `us-co`, `us-tx`.",
            "type": "string"
          },
          "members": {
            "description": "Jurisdiction codes in this group, primary first. Includes the state\nfeed plus its `WZDX_\u003cSTATE\u003e`, optional `WZDX_CWZ_\u003cSTATE\u003e`, and any\nsub-state regional siblings.\n",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "name": {
            "type": "string"
          },
          "primary_jurisdiction_code": {
            "description": "The canonical state/province code (`CA`, `CO`, `TX`). Querying this\ncode on `/events` or `/features` auto-expands to all `members`.\n",
            "type": "string"
          },
          "sort_order": {
            "type": "integer"
          }
        },
        "required": [
          "id",
          "name",
          "primary_jurisdiction_code",
          "members"
        ],
        "type": "object"
      },
      "PagedEvents": {
        "properties": {
          "attribution": {
            "description": "Required data-source credits for the sources present in `data`.\nPresent only for sources whose license mandates a displayed credit\n(e.g. CC-BY, OGL). See `GET /data-sources` for the full catalogue.\n",
            "items": {
              "$ref": "#/components/schemas/SourceCredit"
            },
            "type": "array"
          },
          "data": {
            "items": {
              "$ref": "#/components/schemas/TrafficEvent"
            },
            "type": "array"
          },
          "has_more": {
            "type": "boolean"
          },
          "limit": {
            "type": "integer"
          },
          "offset": {
            "type": "integer"
          },
          "total": {
            "type": "integer"
          },
          "warnings": {
            "description": "Non-fatal notes about the request itself. Present only when there is\nsomething to report, so an existing client sees no change in shape.\n\nTwo families. `unknown_jurisdiction`, `unknown_type`,\n`unknown_severity` and `unknown_status` report a filter value that\ndoes not exist, so nothing can ever match it. `not_covered` and\n`covered_but_empty` appear only on an empty result for a single\njurisdiction and type, and say which kind of empty it is — data we\nhave never carried, versus data we do carry that is quiet right\nnow. `GET /coverage` answers the same question ahead of time.\n\nNone of these reject the request: it still returns 200 with\nwhatever the remaining filters match.\n\nThe same text is also sent as the `X-Jurisdiction-Warning` response\nheader, which is the only channel available on the GeoJSON\nendpoints.\n",
            "items": {
              "$ref": "#/components/schemas/APIWarning"
            },
            "type": "array"
          }
        },
        "type": "object"
      },
      "PagedFeatures": {
        "properties": {
          "attribution": {
            "description": "Required data-source credits for the sources present in `data`.\nPresent only for sources whose license mandates a displayed credit.\nSee `GET /data-sources` for the full catalogue.\n",
            "items": {
              "$ref": "#/components/schemas/SourceCredit"
            },
            "type": "array"
          },
          "data": {
            "items": {
              "$ref": "#/components/schemas/Feature"
            },
            "type": "array"
          },
          "has_more": {
            "description": "Whether more features match beyond this page. Always exact, including\npast the `total` cap.\n",
            "type": "boolean"
          },
          "limit": {
            "type": "integer"
          },
          "offset": {
            "type": "integer"
          },
          "total": {
            "description": "Number of matching features, **counted up to 10,000**. A `total` of\nexactly `10000` means \"10,000 or more\", not exactly ten thousand —\nsome layers run to millions of rows (`truck_restrictions` alone is\n~1.5M in North America) and counting them exactly cannot be done\nwithin the request budget. Under the cap, `total` is exact.\n\n`has_more` stays exact at any depth and is the reliable signal for\n\"is there another page\" — prefer it over comparing against `total`\nwhen paginating deep.\n",
            "type": "integer"
          },
          "warnings": {
            "description": "Non-fatal notes about the request itself. Present only when there is\nsomething to report, so an existing client sees no change in shape.\n\nToday the only warning is `unknown_jurisdiction`: a `jurisdiction`\ncode that does not exist in the catalogue. Such a request is not\nrejected — it returns 200 with whatever the remaining codes match —\nbut nothing can ever match the unknown one, and an empty result is\notherwise indistinguishable from genuinely having no data there.\n\nThe same text is also sent as the `X-Jurisdiction-Warning` response\nheader, which is the only channel available on the GeoJSON\nendpoints.\n",
            "items": {
              "$ref": "#/components/schemas/APIWarning"
            },
            "type": "array"
          }
        },
        "type": "object"
      },
      "Plan": {
        "properties": {
          "allow_analytics": {
            "type": "boolean"
          },
          "allow_geojson": {
            "type": "boolean"
          },
          "allow_truck_data": {
            "type": "boolean"
          },
          "allow_webhooks": {
            "type": "boolean"
          },
          "code": {
            "type": "string"
          },
          "data_delay_sec": {
            "description": "0 = real-time",
            "type": "integer"
          },
          "data_unit_price": {
            "description": "Published per-data-request rate. 0 for Enterprise (unlimited).",
            "type": "number"
          },
          "description": {
            "type": "string"
          },
          "features": {
            "description": "Marketing bullets shown on pricing pages. Admin-managed via\n`/admin/plans/{id}/features`. Resolution order on the frontend:\n`i18n_key` (if the active locale has it) → raw `label` → the key\nstring itself.\n",
            "items": {
              "$ref": "#/components/schemas/PlanFeature"
            },
            "type": "array"
          },
          "id": {
            "type": "integer"
          },
          "is_active": {
            "type": "boolean"
          },
          "is_contact_sales": {
            "description": "When true, the pricing page renders a \"Contact sales\" CTA instead of Paddle checkout.",
            "type": "boolean"
          },
          "is_popular": {
            "description": "Marketing flag — highlights this plan on the pricing page.",
            "type": "boolean"
          },
          "max_batch_size": {
            "description": "Maximum number of ids accepted by batch endpoints\n(`GET /events?ids=…`, `POST /features/details/batch`). 0 = unlimited.\n",
            "type": "integer"
          },
          "max_jurisdictions": {
            "description": "0 = unlimited",
            "type": "integer"
          },
          "max_keys": {
            "type": "integer"
          },
          "max_requests_day": {
            "description": "0 = unlimited",
            "type": "integer"
          },
          "max_results_per_page": {
            "description": "0 = unlimited",
            "type": "integer"
          },
          "max_saved_routes": {
            "description": "Maximum number of persisted (non-expiring) saved routes. 0 = unlimited.",
            "type": "integer"
          },
          "max_webhooks": {
            "description": "0 = unlimited (only meaningful when `allow_webhooks` is true).",
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "paddle_price_id": {
            "description": "Paddle price ID for the monthly billing cadence. NULL for free/trial plans.",
            "type": [
              "string",
              "null"
            ]
          },
          "paddle_price_id_annual": {
            "description": "Paddle price ID for the annual billing cadence. NULL when annual billing isn't offered.",
            "type": [
              "string",
              "null"
            ]
          },
          "price_annual": {
            "description": "Annual price (≈ monthly × 12 × 0.85). NULL for the Free plan.",
            "type": [
              "number",
              "null"
            ]
          },
          "price_monthly": {
            "description": "Monthly price in the currency the Paddle account is configured for.",
            "type": "number"
          },
          "rate_limit_rpm": {
            "type": "integer"
          },
          "routing_max_feature_types": {
            "description": "Cap on how many distinct POI feature types a routing request may select\nvia `enrichment.include_features`. 0 = the `features[]` channel is not\navailable on this plan; N = up to N types; -1 = unlimited.\n",
            "type": "integer"
          },
          "routing_rpm": {
            "description": "Routing-only per-minute rate gate. 0 = no override (falls back to `rate_limit_rpm`).",
            "type": "integer"
          },
          "routing_truck_quota": {
            "description": "Monthly truck-routing call allotment (`POST /routing/route`). 0 = routing not enabled on the plan.",
            "type": "integer"
          },
          "routing_unit_price": {
            "description": "Published per-call routing rate (used for the EU 14-day refund proration).",
            "type": "number"
          },
          "tier_family": {
            "description": "Coarse tier grouping used for display and quota logic.",
            "enum": [
              "free",
              "starter",
              "pro",
              "enterprise"
            ],
            "type": "string"
          },
          "trial_days": {
            "description": "0 = no trial limit",
            "type": "integer"
          }
        },
        "type": "object"
      },
      "PlanFeature": {
        "description": "One marketing bullet on a plan's pricing card. Either `i18n_key` or\n`label` is always populated (DB CHECK constraint). Stable bullets use\n`i18n_key` so translations live in frontend locale files; short-lived\nmarketing copy uses `label` only and ships unchanged in every locale.\n",
        "properties": {
          "i18n_key": {
            "description": "Translation key, e.g. `plan_feature.events_cameras`.",
            "type": [
              "string",
              "null"
            ]
          },
          "icon": {
            "description": "Short icon hint (`check`, `zap`, `shield`, ...) the frontend maps to an SVG.",
            "type": [
              "string",
              "null"
            ]
          },
          "id": {
            "type": "integer"
          },
          "label": {
            "description": "Raw fallback shown when no `i18n_key` is set or the locale lacks it.",
            "type": [
              "string",
              "null"
            ]
          },
          "plan_id": {
            "type": "integer"
          },
          "sort_order": {
            "type": "integer"
          },
          "tooltip": {
            "type": [
              "string",
              "null"
            ]
          },
          "tooltip_i18n_key": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "plan_id",
          "sort_order"
        ],
        "type": "object"
      },
      "SourceCredit": {
        "description": "A single data-source attribution / license entry. `attribution` and `disclaimer` are distinct obligations and either may be present without the other: a credit names who produced the data, a disclaimer is text the source requires you to display verbatim.",
        "properties": {
          "attribution": {
            "description": "The exact credit string this source's license requires you to display.",
            "example": "© Dirección General de Tráfico (DGT) — licensed under CC BY 4.0",
            "type": "string"
          },
          "disclaimer": {
            "description": "A notice the source requires applications using its data to display. Present only for sources that impose one; it is not a credit and does not replace `attribution`. Where present, display it verbatim.",
            "example": "The State of Colorado and CDOT make no representations or warranty as to the completeness, accuracy, timeliness, or content of any data made available through this site.",
            "type": "string"
          },
          "license": {
            "example": "CC-BY 4.0",
            "type": "string"
          },
          "license_url": {
            "example": "https://creativecommons.org/licenses/by/4.0/",
            "format": "uri",
            "type": "string"
          },
          "source_code": {
            "description": "Jurisdiction or program code (e.g. `ESP`, `OSM`, `EIA`).",
            "example": "ESP",
            "type": "string"
          },
          "source_name": {
            "example": "Spain — DGT National Access Point",
            "type": "string"
          },
          "updated_at": {
            "description": "The SOURCE's own last-update timestamp, when the upstream feed publishes one. Absent for most sources, which publish none.\nSeveral open licenses make this mandatory ALONGSIDE the credit string, not instead of it: Licence Ouverte 2.0 requires \"la source ... et la date de la derniere mise a jour\", and the Spanish RISP conditions require citing \"la fecha de la ultima actualizacion de los documentos objeto de la reutilizacion\". Where this field is present, display it with the attribution.\nNote this is the SOURCE's date, not ours. It is unrelated to a row's `last_updated`, which records when we observed the content change, and it may legitimately be older than the response.",
            "example": "2026-08-13T13:16:21Z",
            "format": "date-time",
            "type": "string"
          }
        },
        "type": "object"
      },
      "TrafficEvent": {
        "properties": {
          "affected_roads": {
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "archive_reason": {
            "description": "How the event was archived. NULL while `status='active'`; set\nalongside `archived_at` at archival time.\n\n- `observed` — upstream returned a clean response and the event\n  was not in it, so the per-poll archival path retired it. This\n  is the normal case.\n- `stale_sweep` — the per-poll path could not retire the event\n  because upstream responses were empty or errored (so we cannot\n  prove the event is gone vs. a transient outage). After roughly\n  `poll_interval × sweep_factor` with no fresh observation\n  (default factor = 15), a safety-net sweep forces the event to\n  archived. A persistently high `stale_sweep` ratio on a source\n  signals that the upstream feed often fails to emit clearance\n  records or is unreliable in general — treat the\n  `archived_at` for such events as \"presumed gone by\", not a\n  precise end timestamp.\n- `expired` — the event outlived its own `end_time` by more than\n  the grace period (90 days) while the upstream feed was still\n  publishing it as current. Some feeds are registries that never\n  retire a record, so neither path above can ever fire on them:\n  without this, work orders whose validity window closed years\n  ago would be served as active indefinitely. Because the verdict\n  is derived from the record's own `end_time` on every poll, such\n  an event returns to `active` automatically if upstream later\n  moves the end date forward. Treat `archived_at` as \"first poll\n  at which the event was more than 90 days past its stated end\",\n  not as an observed end — the real end is `end_time`.\n",
            "enum": [
              "observed",
              "stale_sweep",
              "expired",
              null
            ],
            "type": [
              "string",
              "null"
            ]
          },
          "archived_at": {
            "description": "Set the moment our sweep first observed the event missing from the upstream feed.",
            "format": "date-time",
            "type": [
              "string",
              "null"
            ]
          },
          "cause": {
            "description": "Optional underlying operational cause, classified independently of\n`type` (unlike `sub_type`, which is scoped under `type`). A crash\nreported by the source as a closure has `type=closure`,\n`sub_type=lane_closure`, and `cause=accident` — so filtering\n`cause=accident` catches accident-caused events regardless of how the\nsource modeled them. Omitted when no cause could be detected. Additive:\nnever replaces `type`, `sub_type`, or `severity`.\n",
            "enum": [
              "accident",
              "disabled_vehicle",
              "debris",
              "spill",
              "fire",
              "police_activity",
              "animal",
              "congestion",
              "hazard"
            ],
            "type": "string"
          },
          "created_at": {
            "description": "When we first stored this event.",
            "format": "date-time",
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "direction": {
            "type": [
              "string",
              "null"
            ]
          },
          "effective_end_time": {
            "description": "Best available end time for the event. Equals `end_time` when\nthe upstream feed reported one; otherwise falls back to\n`archived_at` (the moment our sweep first observed the event\nhad disappeared from the feed). NULL only for active events\nwith no upstream-reported end — i.e., events that are still\nongoing. Use this field for \"incident duration\" analytics.\n",
            "format": "date-time",
            "type": [
              "string",
              "null"
            ]
          },
          "end_time": {
            "description": "Upstream-reported end time. NULL for events the upstream feed\nnever assigned an explicit end (the common case — most 511 feeds\nsimply drop resolved incidents rather than emitting a final\ntimestamp).\n",
            "format": "date-time",
            "type": [
              "string",
              "null"
            ]
          },
          "estimated_end_time": {
            "format": "date-time",
            "type": [
              "string",
              "null"
            ]
          },
          "estimated_start_time": {
            "format": "date-time",
            "type": [
              "string",
              "null"
            ]
          },
          "id": {
            "type": "string"
          },
          "jurisdiction": {
            "type": "string"
          },
          "lanes_affected": {
            "type": [
              "string",
              "null"
            ]
          },
          "last_updated": {
            "description": "When this event's **content** last changed in our data — not when we\nlast polled it. Re-observing an unchanged event does not move it, so\na long-running roadworks entry can legitimately carry a\n`last_updated` that is months old while still being live and\nconfirmed on every poll. Use it to answer \"what changed since I last\nsynced\", and do not read it as a freshness or liveness signal.\n\nThe comparison covers the fields we publish (type, sub_type, cause,\nstatus, severity, title, description, location, affected_roads,\ndirection, lanes_affected, end_time, the estimated_* times and\nroad_class); it is byte-level, so a re-worded description counts as a\nchange.\n\n`/events` and `/events/geojson` sort by severity first and\n`last_updated` descending second (see the endpoint descriptions).\n\n`Feature.last_updated` means the same thing as of 2026-07-23; it\nused to be poll time.\n",
            "format": "date-time",
            "type": "string"
          },
          "latitude": {
            "type": "number"
          },
          "location": {
            "description": "GeoJSON geometry (Point, LineString, or Polygon) describing where the event applies.",
            "type": "object"
          },
          "longitude": {
            "type": "number"
          },
          "metadata": {
            "description": "Raw upstream attributes preserved as opaque JSON. Shape varies by source.",
            "type": "object"
          },
          "road_class": {
            "enum": [
              "interstate",
              "us_highway",
              "state_highway",
              "local"
            ],
            "type": "string"
          },
          "severity": {
            "enum": [
              "minor",
              "moderate",
              "major",
              "critical"
            ],
            "type": "string"
          },
          "source": {
            "description": "Jurisdiction code",
            "type": "string"
          },
          "source_created_at": {
            "description": "Upstream-reported creation timestamp, when available.",
            "format": "date-time",
            "type": [
              "string",
              "null"
            ]
          },
          "source_id": {
            "type": "string"
          },
          "source_updated_at": {
            "description": "Upstream-reported last-modified timestamp, when available.",
            "format": "date-time",
            "type": [
              "string",
              "null"
            ]
          },
          "start_time": {
            "format": "date-time",
            "type": "string"
          },
          "status": {
            "enum": [
              "active",
              "archived"
            ],
            "type": "string"
          },
          "sub_type": {
            "description": "Optional additive second-level classification under `type`\n(e.g. `type=incident`, `sub_type=accident`). Omitted when the event\ncould not be classified. Never replaces `type` — existing `type`\nfilters are unaffected.\n",
            "enum": [
              "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"
            ],
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "type": {
            "enum": [
              "incident",
              "construction",
              "closure",
              "special_event",
              "weather",
              "road_condition",
              "planned",
              "hazard"
            ],
            "type": "string"
          }
        },
        "type": "object"
      },
      "WeatherCorrelationStats": {
        "properties": {
          "avg_severity": {
            "description": "Weighted severity — critical=4, major=3, moderate=2, else 1.",
            "type": "number"
          },
          "avg_temp_c": {
            "description": "Mean temperature in Celsius, averaged over the normalized\n`temperature_c` values. Snapshots whose source unit could not be\nestablished are excluded from the average rather than mixed in;\nwhen no snapshot in the group has a known unit, the field is omitted.\n\nSensor \"no reading\" markers (some feeds signal an offline sensor with\nan out-of-band value rather than omitting the field) are excluded as\nwell, so the mean reflects real observations only.\n",
            "type": "number"
          },
          "avg_visibility": {
            "description": "Mean visibility across the group's snapshots, in the units the\nsources publish. Not normalized — compare within one jurisdiction.\n",
            "type": "number"
          },
          "avg_wind_speed": {
            "description": "Mean wind speed across the group's snapshots, in the units the\nsources publish. Not normalized — compare within one jurisdiction.\n",
            "type": "number"
          },
          "event_count": {
            "type": "integer"
          },
          "precipitation": {
            "type": "string"
          },
          "road_surface": {
            "type": "string"
          }
        },
        "type": "object"
      }
    },
    "securitySchemes": {
      "ApiKeyHeader": {
        "in": "header",
        "name": "X-API-Key",
        "type": "apiKey"
      },
      "ApiKeyQuery": {
        "in": "query",
        "name": "api_key",
        "type": "apiKey"
      },
      "BearerAuth": {
        "bearerFormat": "JWT",
        "scheme": "bearer",
        "type": "http"
      }
    }
  },
  "info": {
    "contact": {
      "email": "support@road511.com"
    },
    "description": "Aggregated North American 511 traffic data API. Normalizes data from 45+ US state\nand Canadian province 511 systems into a single REST API.\n\n## Authentication\n- **API Key**: required on all data endpoints — pass via `X-API-Key` header or `api_key` query\n  parameter. Only the public map/metadata endpoints (`/map/*`, `/jurisdictions`,\n  `/jurisdictions/groups`, `/stats`, `/stats/summary`, `/status`, `/data-sources`, `/health`)\n  need no key.\n- **Admin**: Bearer JWT token via `Authorization: Bearer \u003ctoken\u003e`\n- **Customer Portal**: Bearer JWT token via `Authorization: Bearer \u003ctoken\u003e`\n\n## Plan Limits\n| Plan | RPM | Daily Requests | Jurisdictions | Results/Page | GeoJSON | Analytics | Truck | Data Delay | Trial |\n|------|-----|----------------|---------------|--------------|---------|-----------|-------|------------|-------|\n| Free | 20 | 1,000 | 2 per request | 100 | No | No | No | 15 min | 14 days |\n| Starter | 300 | 50,000 | Unlimited | 500 | Yes | No | No | Real-time | - |\n| Pro | 1,000 | 500,000 | Unlimited | 1,000 | Yes | Yes | Yes | Real-time | - |\n| Enterprise | 5,000 | Unlimited | Unlimited | Unlimited | Yes | Yes | Yes | Real-time | - |\n\nEnterprise includes up to 50 API keys.\n",
    "license": {
      "name": "Proprietary"
    },
    "title": "Road511 Traffic API",
    "version": "1.0.0"
  },
  "openapi": "3.1.0",
  "paths": {
    "/analytics/changes": {
      "get": {
        "description": "Latest event lifecycle changes across all jurisdictions. Requires\nPro+ plan. Archival entries carry `new_value=archived`,\n`new_value=stale_sweep` or `new_value=expired` — see\n`archive_reason` on the `TrafficEvent` schema for what each means.\n",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": [
                    {
                      "change_type": "severity_change",
                      "event_id": "on-evt-401-001",
                      "event_type": "incident",
                      "id": 45,
                      "jurisdiction": "ON",
                      "new_value": "major",
                      "old_value": "moderate",
                      "recorded_at": "2026-03-29T14:35:00Z",
                      "road_name": "Highway 401",
                      "severity": "major",
                      "source": "ON"
                    },
                    {
                      "change_type": "end_time_change",
                      "event_id": "ga-evt-i75-002",
                      "event_type": "construction",
                      "id": 44,
                      "jurisdiction": "GA",
                      "new_value": "2026-04-15T18:00:00Z",
                      "old_value": "2026-04-10T18:00:00Z",
                      "recorded_at": "2026-03-29T10:00:00Z",
                      "road_name": "I-75",
                      "severity": "moderate",
                      "source": "ga"
                    }
                  ],
                  "has_more": false,
                  "limit": 100,
                  "offset": 0,
                  "total": 2
                }
              }
            },
            "description": "Paginated recent changes"
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "error": "analytics access requires Pro plan or higher"
                }
              }
            },
            "description": "Analytics not available on current plan"
          }
        },
        "summary": "Recent changes feed",
        "tags": [
          "analytics"
        ]
      }
    },
    "/analytics/clearance": {
      "get": {
        "description": "Event clearance time statistics. Requires Pro+ plan.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": [
                  {
                    "avg_minutes": 62.8,
                    "count": 320,
                    "event_type": "incident",
                    "jurisdiction": "ON",
                    "max_minutes": 540,
                    "min_minutes": 8,
                    "p50_minutes": 45.2,
                    "p95_minutes": 180.5,
                    "severity": "major"
                  },
                  {
                    "avg_minutes": 7200,
                    "count": 150,
                    "event_type": "construction",
                    "jurisdiction": "GA",
                    "max_minutes": 43200,
                    "min_minutes": 360,
                    "p50_minutes": 4320,
                    "p95_minutes": 21600,
                    "severity": "moderate"
                  }
                ]
              }
            },
            "description": "Clearance statistics"
          }
        },
        "summary": "Clearance times (P50/P95)",
        "tags": [
          "analytics"
        ]
      }
    },
    "/analytics/corridors": {
      "get": {
        "description": "Event frequency and clearance by road corridor. Requires Pro+ plan.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": [
                  {
                    "avg_severity": 2.6,
                    "event_count": 245,
                    "jurisdiction": "ON",
                    "last_event": "2026-03-29T14:35:00Z",
                    "road_name": "Highway 401"
                  },
                  {
                    "avg_severity": 2.1,
                    "event_count": 112,
                    "jurisdiction": "GA",
                    "last_event": "2026-03-29T11:08:00Z",
                    "road_name": "I-75"
                  }
                ]
              }
            },
            "description": "Corridor stats"
          }
        },
        "summary": "Corridor reliability",
        "tags": [
          "analytics"
        ]
      }
    },
    "/analytics/event-weather/{id}": {
      "get": {
        "description": "Returns the weather conditions near an event at the time it was created.\nCaptures nearest weather station data (temperature, wind, visibility,\nprecipitation, road surface, humidity) via PostGIS spatial query.\nRequires Pro+ plan.\n",
        "parameters": [
          {
            "description": "Event ID",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "created_at": "2026-03-29T14:30:00Z",
                  "distance_km": 3.2,
                  "event_id": "on-evt-401-001",
                  "humidity": 88,
                  "id": 42,
                  "precipitation": "snow",
                  "reading_recorded_at": "2026-03-29T14:28:00Z",
                  "road_surface": "icy",
                  "temperature": -2.5,
                  "visibility": 1.5,
                  "wind_direction": "NW",
                  "wind_speed": 35
                },
                "schema": {
                  "$ref": "#/components/schemas/EventWeatherSnapshot"
                }
              }
            },
            "description": "Weather snapshot for the event"
          },
          "404": {
            "content": {
              "application/json": {
                "example": {
                  "error": "no weather snapshot for this event"
                }
              }
            },
            "description": "No weather snapshot for this event"
          }
        },
        "summary": "Event weather snapshot",
        "tags": [
          "analytics"
        ]
      }
    },
    "/analytics/feature-history": {
      "get": {
        "description": "Lifecycle and schedule changes for features. Requires Pro+ plan.\n\nFilterable by `feature_id`, `source`, `feature_type`, `change_type`,\n`from`, `to`, plus `limit`/`offset`. `change_type` is one of\n`created`, `deactivated`, `reactivated`, `start_time_change`,\n`end_time_change`, `estimated_start_time_change`,\n`estimated_end_time_change` — the last two are how an agency\nrescheduling a planned job shows up as data.\n",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": [
                    {
                      "change_type": "deactivated",
                      "changed_field": "is_active",
                      "feature_id": "on-cam-401-yonge",
                      "feature_type": "cameras",
                      "id": 4011,
                      "new_value": "false",
                      "old_value": "true",
                      "recorded_at": "2026-03-15T08:00:00Z",
                      "source": "ON"
                    },
                    {
                      "change_type": "reactivated",
                      "changed_field": "is_active",
                      "feature_id": "on-cam-401-yonge",
                      "feature_type": "cameras",
                      "id": 4012,
                      "new_value": "true",
                      "old_value": "false",
                      "recorded_at": "2026-03-16T06:00:00Z",
                      "source": "ON"
                    }
                  ],
                  "has_more": false,
                  "limit": 100,
                  "offset": 0,
                  "total": 2
                }
              }
            },
            "description": "Paginated feature history entries"
          }
        },
        "summary": "Feature history",
        "tags": [
          "analytics"
        ]
      }
    },
    "/analytics/forecast": {
      "get": {
        "description": "Empirical incident probability and expected delay for a road segment by day-of-week and hour, derived from historical aggregates. Omit dow/hour to get the full 7x24 grid. Thin segments degrade to corridor then jurisdiction basis; sample_size, confidence and basis report how much data backs each bucket. Requires Pro+ plan.\n`expected_delay_min` and `p95_delay_min` count only unplanned event types (incident, closure, weather) — a three-week work zone is reported by `probability`, not as a three-week delay — and each contributing event is capped at 4320 minutes (72 hours). A value at 4320 therefore means \"at least 72 hours\", not exactly 72. Both fields are omitted when no duration could be established; the event still counts toward `probability` and `expected_count`.",
        "parameters": [
          {
            "description": "Jurisdiction code (e.g. GA).",
            "in": "query",
            "name": "jurisdiction",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Road name (e.g. I-85).",
            "in": "query",
            "name": "road",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Optional road class filter.",
            "in": "query",
            "name": "road_class",
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Day of week (0=Sunday..6=Saturday). Omit for the full grid.",
            "in": "query",
            "name": "dow",
            "schema": {
              "maximum": 6,
              "minimum": 0,
              "type": "integer"
            }
          },
          {
            "description": "Hour of day (0-23). Omit for the full grid.",
            "in": "query",
            "name": "hour",
            "schema": {
              "maximum": 23,
              "minimum": 0,
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "bucket": {
                    "basis": "segment",
                    "confidence": "high",
                    "dow": 1,
                    "expected_count": 0.6,
                    "expected_delay_min": 47.2,
                    "hour": 17,
                    "p95_delay_min": 120,
                    "probability": 0.35,
                    "probability_ci": [
                      0.18,
                      0.57
                    ],
                    "sample_size": 12
                  },
                  "computed_at": "2026-06-11T03:00:00Z",
                  "jurisdiction": "GA",
                  "road": "I-85",
                  "road_class": "interstate",
                  "window_days": 90
                }
              }
            },
            "description": "Segment forecast — a single bucket when dow+hour are pinned, otherwise the full grid in buckets[]"
          }
        },
        "summary": "Incident-probability forecast",
        "tags": [
          "analytics"
        ]
      }
    },
    "/analytics/history/{id}": {
      "get": {
        "description": "Lifecycle changes for a specific event. Requires Pro+ plan.\n\n`change_type` values include `created`, `severity_change`,\n`lanes_change`, `end_time_change`, `estimated_end_time_change`,\n`archived`, `stale_sweep`, and `expired`. The last three all mean\nthe event is no longer active — see `archive_reason` on the\n`TrafficEvent` schema for the distinction.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": [
                  {
                    "change_type": "created",
                    "event_id": "on-evt-401-001",
                    "event_type": "incident",
                    "id": 0,
                    "jurisdiction": "ON",
                    "recorded_at": "2026-03-29T14:30:00Z",
                    "road_name": "Highway 401",
                    "severity": "moderate",
                    "source": "ON"
                  },
                  {
                    "change_type": "severity_change",
                    "event_id": "on-evt-401-001",
                    "event_type": "incident",
                    "id": 2,
                    "jurisdiction": "ON",
                    "new_value": "major",
                    "old_value": "moderate",
                    "recorded_at": "2026-03-29T14:35:00Z",
                    "road_name": "Highway 401",
                    "severity": "major",
                    "source": "ON"
                  },
                  {
                    "change_type": "lanes_change",
                    "event_id": "on-evt-401-001",
                    "event_type": "incident",
                    "id": 3,
                    "jurisdiction": "ON",
                    "new_value": "2 of 4 lanes blocked",
                    "old_value": "1 of 4 lanes blocked",
                    "recorded_at": "2026-03-29T14:40:00Z",
                    "road_name": "Highway 401",
                    "severity": "major",
                    "source": "ON"
                  }
                ]
              }
            },
            "description": "Array of history entries"
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "error": "analytics access requires Pro plan or higher"
                }
              }
            },
            "description": "Analytics not available on current plan"
          }
        },
        "summary": "Event history timeline",
        "tags": [
          "analytics"
        ]
      }
    },
    "/analytics/hotspots": {
      "get": {
        "description": "Geographic clustering of events. Requires Pro+ plan.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": [
                  {
                    "avg_severity": 2.8,
                    "event_count": 28,
                    "latitude": 43.651,
                    "longitude": -79.347,
                    "top_road": "Highway 401"
                  },
                  {
                    "avg_severity": 2.2,
                    "event_count": 15,
                    "latitude": 33.758,
                    "longitude": -84.392,
                    "top_road": "I-75"
                  }
                ]
              }
            },
            "description": "Hotspot clusters"
          }
        },
        "summary": "Hotspot clustering",
        "tags": [
          "analytics"
        ]
      }
    },
    "/analytics/scorecard": {
      "get": {
        "description": "Per-jurisdiction rollup (incident volume, clearance speed, severity mix, construction schedule drift, road-condition coverage, source uptime) with a 0-100 composite score, optionally ranked. Returns all active jurisdictions by default. Requires Pro+ plan.\n\n**Construction fields.** `construction_overrun_avg_days` is the average **net movement of the published end date** — the last end date the source published for a job minus the first one it published, in days. Positive means jobs are typically pushed later, so a DOT reporting +6 has historically needed about six days more than it first advertised. It is derived from the recorded per-revision history, not from when the job disappeared from the feed.\n\n`construction_reschedule_avg` counts only **material** reschedules — moves of at least one day. Feeds that publish a rolling ETA (an end time creeping forward by minutes on every poll) are not rescheduling anything, and those steps are excluded.\n\nJobs whose end-date revision history is no longer retained are **omitted from both averages** rather than counted as zero. Both figures therefore describe jobs with observable revisions; `construction_jobs` is the full count of tracked jobs and will usually be larger.",
        "parameters": [
          {
            "description": "Restrict to one jurisdiction code (e.g. GA).",
            "in": "query",
            "name": "jurisdiction",
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Filter by country.",
            "in": "query",
            "name": "country",
            "schema": {
              "enum": [
                "US",
                "CA"
              ],
              "type": "string"
            }
          },
          {
            "description": "Window start (YYYY-MM-DD, inclusive).",
            "in": "query",
            "name": "from",
            "schema": {
              "format": "date",
              "type": "string"
            }
          },
          {
            "description": "Window end (YYYY-MM-DD, exclusive upper bound).",
            "in": "query",
            "name": "to",
            "schema": {
              "format": "date",
              "type": "string"
            }
          },
          {
            "description": "Ranking metric.",
            "in": "query",
            "name": "rank_by",
            "schema": {
              "default": "score",
              "enum": [
                "score",
                "incident_volume",
                "avg_clearance_minutes",
                "source_uptime",
                "construction_overrun_avg_days"
              ],
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "order",
            "schema": {
              "default": "desc",
              "enum": [
                "asc",
                "desc"
              ],
              "type": "string"
            }
          },
          {
            "description": "Max rows; defaults to all ~90 jurisdictions.",
            "in": "query",
            "name": "limit",
            "schema": {
              "default": 200,
              "maximum": 200,
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": [
                  {
                    "avg_clearance_minutes": 48.2,
                    "avg_severity": 2.4,
                    "construction_jobs": 54,
                    "construction_overrun_avg_days": 3.1,
                    "construction_reschedule_avg": 0.7,
                    "country": "CA",
                    "event_volume": 1240,
                    "incident_volume": 612,
                    "jurisdiction": "ON",
                    "name": "Ontario",
                    "p95_clearance_minutes": 190,
                    "rank": 1,
                    "road_condition_features": 320,
                    "road_condition_updates": 1450,
                    "score": 87.4,
                    "severity_mix": {
                      "critical": 18,
                      "major": 140,
                      "minor": 680,
                      "moderate": 402
                    },
                    "source_uptime": 0.991,
                    "window": {
                      "from": "2026-05-01",
                      "to": "2026-06-11"
                    }
                  }
                ]
              }
            },
            "description": "Ranked jurisdiction scorecards"
          }
        },
        "summary": "Jurisdiction scorecard",
        "tags": [
          "analytics"
        ]
      }
    },
    "/analytics/trends": {
      "get": {
        "description": "Time-series event counts. Requires Pro+ plan.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": [
                  {
                    "count": 58,
                    "timestamp": "2026-03-29T00:00:00Z",
                    "type": "incident"
                  },
                  {
                    "count": 62,
                    "timestamp": "2026-03-29T00:00:00Z",
                    "type": "construction"
                  },
                  {
                    "count": 45,
                    "timestamp": "2026-03-28T00:00:00Z",
                    "type": "incident"
                  },
                  {
                    "count": 60,
                    "timestamp": "2026-03-28T00:00:00Z",
                    "type": "construction"
                  }
                ]
              }
            },
            "description": "Trend data points"
          }
        },
        "summary": "Event trends",
        "tags": [
          "analytics"
        ]
      }
    },
    "/analytics/weather": {
      "get": {
        "description": "Historical weather station data. Requires Pro+ plan.\n\n`temperature` is the value as the reporting agency publishes it;\n`temperature_c` is that value normalized to Celsius, and is omitted when\nthe feed's unit has not been established.\n",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": [
                  {
                    "feature_id": "MD-wx-i95-balt",
                    "humidity": 62,
                    "id": 88412,
                    "precipitation": "none",
                    "recorded_at": "2026-03-29T14:45:00Z",
                    "road_surface": "dry",
                    "source": "MD",
                    "temperature": 65.3,
                    "temperature_c": 18.5,
                    "visibility": 16,
                    "wind_direction": "NW",
                    "wind_speed": 12.3
                  }
                ]
              }
            },
            "description": "Weather readings"
          }
        },
        "summary": "Weather readings history",
        "tags": [
          "analytics"
        ]
      }
    },
    "/analytics/weather-correlation": {
      "get": {
        "description": "Returns aggregated weather conditions across events, grouped by\nprecipitation type and road surface. Shows how weather conditions\ncorrelate with incident frequency and severity. Requires Pro+ plan.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/jurisdiction"
          },
          {
            "description": "Filter by event type",
            "in": "query",
            "name": "type",
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "severity",
            "schema": {
              "enum": [
                "minor",
                "moderate",
                "major",
                "critical"
              ],
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "from",
            "schema": {
              "format": "date",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "to",
            "schema": {
              "format": "date",
              "type": "string"
            }
          },
          {
            "description": "Max station distance in km (default 50)",
            "in": "query",
            "name": "max_dist_km",
            "schema": {
              "default": 50,
              "type": "number"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "default": 50,
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": [
                  {
                    "avg_severity": 3.2,
                    "avg_temp_c": -5.1,
                    "avg_visibility": 2.3,
                    "avg_wind_speed": 28.4,
                    "event_count": 84,
                    "precipitation": "snow",
                    "road_surface": "icy"
                  },
                  {
                    "avg_severity": 2.1,
                    "avg_temp_c": 8.3,
                    "avg_visibility": 8.5,
                    "avg_wind_speed": 18.7,
                    "event_count": 156,
                    "precipitation": "rain",
                    "road_surface": "wet"
                  },
                  {
                    "avg_severity": 1.8,
                    "avg_temp_c": 15.2,
                    "avg_visibility": 16,
                    "avg_wind_speed": 10.1,
                    "event_count": 320,
                    "precipitation": "none",
                    "road_surface": "dry"
                  }
                ],
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/WeatherCorrelationStats"
                  },
                  "type": "array"
                }
              }
            },
            "description": "Correlation statistics"
          }
        },
        "summary": "Weather-incident correlation",
        "tags": [
          "analytics"
        ]
      }
    },
    "/coverage": {
      "get": {
        "description": "Answers \"is it worth asking?\" before a query comes back empty.\n\nAn empty result from `/events` or `/features` has always meant one of\ntwo opposite things — we have never carried that data, or we carry it\nand there is nothing right now — and nothing in the response separated\nthem. This endpoint does.\n\nPublic metadata — no API key required.\n\nReads a summary the worker rebuilds hourly, so the answer is up to\n`COVERAGE_REFRESH_INTERVAL` stale: a type that started producing\nminutes ago can still read as empty.\n\n**What it does not tell you:** why something is empty. A seasonal feed\nthat is quiet by design and one that broke an hour ago look identical;\n`latest_data_at` narrows that and does not settle it. Nor does listing a\njurisdiction here mean your plan may query it — coverage is about the\ndata we hold, not entitlement.\n",
        "parameters": [
          {
            "description": "A single jurisdiction code. Omit it to get only the summary\nmetadata — the per-type detail is over a thousand rows and is not\nreturned unfiltered.\n",
            "example": "FRA",
            "in": "query",
            "name": "jurisdiction",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "covered": true,
                  "events": [
                    {
                      "last_nonzero_at": "2026-08-26T12:00:00Z",
                      "latest_data_at": "2026-08-26T11:59:31Z",
                      "row_count": 92,
                      "type": "incident"
                    }
                  ],
                  "features": [
                    {
                      "last_nonzero_at": "2026-08-26T12:00:00Z",
                      "latest_data_at": "2026-08-26T11:58:04Z",
                      "row_count": 1820,
                      "type": "cameras"
                    },
                    {
                      "last_nonzero_at": "2026-03-01T12:00:00Z",
                      "row_count": 0,
                      "type": "ev_charging"
                    }
                  ],
                  "jurisdiction": "FRA",
                  "refreshed_at": "2026-08-26T12:00:00Z",
                  "summary_rows": 1445
                },
                "schema": {
                  "properties": {
                    "covered": {
                      "description": "False means we have never held anything for this jurisdiction — not that it is currently empty.",
                      "type": "boolean"
                    },
                    "events": {
                      "items": {
                        "$ref": "#/components/schemas/CoverageEntry"
                      },
                      "type": "array"
                    },
                    "features": {
                      "items": {
                        "$ref": "#/components/schemas/CoverageEntry"
                      },
                      "type": "array"
                    },
                    "jurisdiction": {
                      "example": "FRA",
                      "type": "string"
                    },
                    "refreshed_at": {
                      "description": "When the in-memory summary was last rebuilt.",
                      "format": "date-time",
                      "type": "string"
                    },
                    "summary_rows": {
                      "description": "Number of (jurisdiction, type) pairs in the summary.",
                      "type": "integer"
                    },
                    "warnings": {
                      "items": {
                        "$ref": "#/components/schemas/APIWarning"
                      },
                      "type": "array"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Coverage for the requested jurisdiction"
          },
          "503": {
            "description": "The coverage summary has not loaded yet. Returned rather than an empty body, which would read as \"we cover nothing\"."
          }
        },
        "summary": "What we hold, per jurisdiction and type",
        "tags": [
          "metadata"
        ]
      }
    },
    "/data-sources": {
      "get": {
        "description": "Returns the licensing and attribution catalogue for every data source.\nPublic — no API key required.\n\nUse this to render the credits your application must display. Sources\nunder open licenses that require attribution (CC-BY, OGL v3.0, ODbL,\nNLOD, Licence Ouverte, DL-DE→BY) carry the exact credit string in\n`attribution`. List responses (`/events`, `/features`) also embed an\ninline `attribution[]` for the sources present in that response.\n",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "data": {
                      "items": {
                        "$ref": "#/components/schemas/SourceCredit"
                      },
                      "type": "array"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "The data-source catalogue"
          }
        },
        "security": [],
        "summary": "Data-source licensing \u0026 attribution catalogue",
        "tags": [
          "data-sources"
        ]
      }
    },
    "/events": {
      "get": {
        "description": "Paginated list of traffic events with filters. Free plan requires\njurisdiction parameter.\n\n**Ordering** (not configurable): severity first — `critical`, `major`,\n`moderate`, then everything else — and within a severity band by\n`last_updated` descending, tie-broken by `id` descending. Because\n`last_updated` is a content-change time, the top of a band is what\nchanged most recently, not what we polled most recently.\n\nThe `id` tiebreak matters for pagination: `last_updated` alone is not a\ntotal order (events land in batches that share a timestamp), so without\nit two pages could repeat or skip rows.\n\n**Batch lookup**: pass `?ids=id1,id2,...` to fetch a known set of\nevents by id. When `ids` is set, all other filters except plan\ndata delay are ignored and pagination is disabled — the response\nalways returns the full set in one page. Archived events are\nfiltered per plan: non-analytics plans\n(`allow_analytics: false`) receive only events whose effective\nstart_time is within the last 7 days. Active events are always\nreturned regardless of age. `total` reflects the post-filter\ncount. The batch size is capped by the plan's `max_batch_size`.\n",
        "parameters": [
          {
            "description": "Comma-separated event IDs for batch lookup. Mutually exclusive\nwith the filter parameters below (when present, other filters\nare ignored). Bounded by the plan's `max_batch_size`.\n",
            "in": "query",
            "name": "ids",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/jurisdiction"
          },
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          },
          {
            "$ref": "#/components/parameters/bbox"
          },
          {
            "$ref": "#/components/parameters/lat"
          },
          {
            "$ref": "#/components/parameters/lng"
          },
          {
            "$ref": "#/components/parameters/radius_km"
          },
          {
            "description": "Filter by event type",
            "in": "query",
            "name": "type",
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Filter by second-level classification (e.g. `accident`). Additive —\nomit to match all sub_types. See the `sub_type` enum on the\nTrafficEvent schema for the full vocabulary.\n",
            "in": "query",
            "name": "sub_type",
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Filter by underlying operational cause, independent of `type` —\ne.g. `cause=accident` returns accident-caused events whether the\nsource modeled them as an incident, closure, or construction. Additive;\nomit to match all causes. See the `cause` enum on the TrafficEvent\nschema.\n",
            "in": "query",
            "name": "cause",
            "schema": {
              "enum": [
                "accident",
                "disabled_vehicle",
                "debris",
                "spill",
                "fire",
                "police_activity",
                "animal",
                "congestion",
                "hazard"
              ],
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "severity",
            "schema": {
              "enum": [
                "minor",
                "moderate",
                "major",
                "critical"
              ],
              "type": "string"
            }
          },
          {
            "description": "`active` (default) returns active events, `archived` returns\narchived events only, `all` disables the filter and returns both.\n\nPlans without analytics access (`allow_analytics: false`) may\nrequest `archived` or `all` only when paired with a\n`start_time_from` no older than 7 days — otherwise the\nrequest is rejected with 403. Plans with analytics access\nhave no such restriction.\n",
            "in": "query",
            "name": "status",
            "schema": {
              "default": "active",
              "enum": [
                "active",
                "archived",
                "all"
              ],
              "type": "string"
            }
          },
          {
            "description": "Filter by road name (partial match)",
            "in": "query",
            "name": "road",
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Lower bound (inclusive, RFC3339) on the event's start time.\nMatches `COALESCE(start_time, created_at)`, so events whose\nupstream feed never populated `start_time` still surface via\ntheir ingest time. When either time-range parameter is set\nand `status` is omitted, the default flips from `active` to\n`all` so archived (historical) events are returned. Lookback\nbeyond the last 7 days requires a plan with analytics\naccess (`allow_analytics: true`).\n",
            "in": "query",
            "name": "start_time_from",
            "schema": {
              "format": "date-time",
              "type": "string"
            }
          },
          {
            "description": "Upper bound (inclusive, RFC3339) on the event's start time.\nSee `start_time_from` for column semantics. Without a\n`start_time_from` companion the range is treated as\nopen-ended on the lower end and therefore counts as deep\nhistory (analytics plan required).\n",
            "in": "query",
            "name": "start_time_to",
            "schema": {
              "format": "date-time",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": [
                    {
                      "affected_roads": [
                        "Highway 401"
                      ],
                      "created_at": "2026-03-29T14:30:00Z",
                      "description": "Two right lanes blocked. Emergency services on scene. Expect delays of 30+ minutes.",
                      "direction": "westbound",
                      "end_time": null,
                      "estimated_end_time": "2026-03-29T17:00:00Z",
                      "id": "on-evt-401-001",
                      "jurisdiction": "ON",
                      "lanes_affected": "2 of 4 lanes blocked",
                      "last_updated": "2026-03-29T14:45:00Z",
                      "latitude": 43.65,
                      "location": {
                        "coordinates": [
                          -79.38,
                          43.65
                        ],
                        "type": "Point"
                      },
                      "longitude": -79.38,
                      "road_class": "interstate",
                      "severity": "major",
                      "source": "on",
                      "source_id": "EVT-2026-04521",
                      "start_time": "2026-03-29T14:30:00Z",
                      "status": "active",
                      "title": "Multi-vehicle collision on Highway 401 WB near Yonge St",
                      "type": "incident"
                    },
                    {
                      "affected_roads": [
                        "I-75"
                      ],
                      "created_at": "2026-03-25T06:00:00Z",
                      "description": "Road resurfacing. Right lane closed from mile marker 248 to 251.",
                      "direction": "northbound",
                      "end_time": null,
                      "estimated_end_time": "2026-04-15T18:00:00Z",
                      "id": "ga-evt-i75-002",
                      "jurisdiction": "GA",
                      "lanes_affected": "1 of 3 lanes closed",
                      "last_updated": "2026-03-29T08:00:00Z",
                      "latitude": 33.75,
                      "location": {
                        "coordinates": [
                          -84.39,
                          33.75
                        ],
                        "type": "Point"
                      },
                      "longitude": -84.39,
                      "road_class": "interstate",
                      "severity": "moderate",
                      "source": "ga",
                      "source_id": "GA-CONST-8832",
                      "start_time": "2026-03-25T06:00:00Z",
                      "status": "active",
                      "title": "Lane closure on I-75 NB near Exit 249",
                      "type": "construction"
                    }
                  ],
                  "has_more": false,
                  "limit": 100,
                  "offset": 0,
                  "total": 2
                },
                "schema": {
                  "$ref": "#/components/schemas/PagedEvents"
                }
              }
            },
            "description": "Paginated events"
          },
          "403": {
            "content": {
              "application/json": {
                "examples": {
                  "archivedStatusGate": {
                    "summary": "Archived/all status without a recent start_time_from",
                    "value": {
                      "error": "status=archived requires start_time_from within the last 168h0m0s on the Free plan, or a plan with analytics access"
                    }
                  },
                  "jurisdictionRequired": {
                    "summary": "Free plan requires jurisdiction",
                    "value": {
                      "error": "jurisdiction parameter required on Free plan"
                    }
                  },
                  "timeRangeGate": {
                    "summary": "start_time_from older than 7 days",
                    "value": {
                      "error": "historical event search beyond 168h0m0s requires a plan with analytics access — the Free plan only allows the last 168h0m0s"
                    }
                  }
                }
              }
            },
            "description": "Plan limit exceeded. Common causes: jurisdiction required on\nFree, trial expired, `start_time_from`/`start_time_to`\nreaches beyond 7 days without analytics access,\n`status=archived` or `status=all` requested without a\n`start_time_from` inside the 7-day window.\n"
          }
        },
        "summary": "List traffic events",
        "tags": [
          "events"
        ]
      }
    },
    "/events/geojson": {
      "get": {
        "description": "Returns events as a GeoJSON FeatureCollection. Requires Starter+ plan.\n\n**Ordering** (not configurable): the same as `/events` — severity first,\nthen `last_updated` descending, tie-broken by `id` descending.\n\nThis matters more here than on `/events`, because this endpoint always\ntruncates at `limit` and has no pagination: the ordering decides what a\nmap can draw at all. Severity-first guarantees the most serious events\nsurvive the cut. It was previously ordered by `last_updated` alone,\nwhich on a large active set could return a page containing no critical\nevents whatsoever.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/jurisdiction"
          },
          {
            "$ref": "#/components/parameters/bbox"
          },
          {
            "$ref": "#/components/parameters/lat"
          },
          {
            "$ref": "#/components/parameters/lng"
          },
          {
            "$ref": "#/components/parameters/radius_km"
          },
          {
            "in": "query",
            "name": "type",
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "severity",
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "`active` (default) returns active events, `archived` returns\narchived events only, `all` disables the filter and returns both.\n\n`/events/geojson` exposes no `start_time_from` parameter, so\nfor callers on plans without analytics access\n(`allow_analytics: false`) the effective allowed values\ncollapse to `active` only — requesting `archived` or `all`\nreturns 403. Plans with analytics access are unrestricted.\n",
            "in": "query",
            "name": "status",
            "schema": {
              "default": "active",
              "enum": [
                "active",
                "archived",
                "all"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/geo+json": {
                "example": {
                  "features": [
                    {
                      "geometry": {
                        "coordinates": [
                          -79.38,
                          43.65
                        ],
                        "type": "Point"
                      },
                      "properties": {
                        "affected_roads": [
                          "Highway 401"
                        ],
                        "direction": "westbound",
                        "id": "on-evt-401-001",
                        "jurisdiction": "ON",
                        "severity": "major",
                        "source": "on",
                        "start_time": "2026-03-29T14:30:00Z",
                        "status": "active",
                        "title": "Multi-vehicle collision on Highway 401 WB near Yonge St",
                        "type": "incident"
                      },
                      "type": "Feature"
                    }
                  ],
                  "type": "FeatureCollection"
                },
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "GeoJSON FeatureCollection"
          },
          "403": {
            "content": {
              "application/json": {
                "examples": {
                  "archivedStatusGate": {
                    "summary": "Archived status requires analytics access",
                    "value": {
                      "error": "status=archived requires start_time_from within the last 168h0m0s on the Starter plan, or a plan with analytics access"
                    }
                  },
                  "geojsonGate": {
                    "summary": "GeoJSON not available on Free",
                    "value": {
                      "error": "GeoJSON access requires Starter plan or higher"
                    }
                  }
                }
              }
            },
            "description": "GeoJSON requires Starter+, or `status=archived`/`all` was\nrequested without analytics access.\n"
          }
        },
        "summary": "Events as GeoJSON",
        "tags": [
          "events"
        ]
      }
    },
    "/events/{id}": {
      "get": {
        "description": "Returns full detail for a single event. Active events are always\nreturned. Archived events are gated by plan: on plans without\nanalytics access (`allow_analytics: false`) the request is\nrejected with 403 when the event's effective start_time\n(`COALESCE(start_time, created_at)`) is older than 7 days.\nPlans with analytics access have no such restriction.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "affected_roads": [
                    "Highway 401"
                  ],
                  "created_at": "2026-03-29T14:30:00Z",
                  "description": "Two right lanes blocked. Emergency services on scene. Expect delays of 30+ minutes.",
                  "direction": "westbound",
                  "end_time": null,
                  "estimated_end_time": "2026-03-29T17:00:00Z",
                  "id": "on-evt-401-001",
                  "jurisdiction": "ON",
                  "lanes_affected": "2 of 4 lanes blocked",
                  "last_updated": "2026-03-29T14:45:00Z",
                  "latitude": 43.65,
                  "location": {
                    "coordinates": [
                      -79.38,
                      43.65
                    ],
                    "type": "Point"
                  },
                  "longitude": -79.38,
                  "road_class": "interstate",
                  "severity": "major",
                  "source": "on",
                  "source_id": "EVT-2026-04521",
                  "start_time": "2026-03-29T14:30:00Z",
                  "status": "active",
                  "title": "Multi-vehicle collision on Highway 401 WB near Yonge St",
                  "type": "incident"
                },
                "schema": {
                  "$ref": "#/components/schemas/TrafficEvent"
                }
              }
            },
            "description": "Event detail"
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "error": "event on-evt-401-001 is archived beyond 168h0m0s — historical detail requires a plan with analytics access (current plan: Free)"
                }
              }
            },
            "description": "Archived event beyond the caller's plan window."
          },
          "404": {
            "content": {
              "application/json": {
                "example": {
                  "error": "event not found"
                }
              }
            },
            "description": "Event not found"
          }
        },
        "summary": "Get event by ID",
        "tags": [
          "events"
        ]
      }
    },
    "/features": {
      "get": {
        "description": "Paginated features filtered by type or group. Either `type` or `group` is required.\nFree plan requires jurisdiction parameter.\n\nTime-bounded feature types (`future_construction`, `alerts`, `special_events`,\n`truck_restrictions`, …) additionally support the scheduled-window filters\n`starts_after` / `starts_before` / `ends_after` / `ends_before`.\n\n**Reading a whole set: use `cursor`, not `offset`.** Every response that\nhas a next page carries `next_cursor`; pass it back as `cursor` and omit\n`offset`. This works at any depth, while `offset` is capped at 50,000 —\nand several single (type, jurisdiction) combinations are larger than\nthat, so `offset` alone cannot read them to the end. You can switch\nmid-walk: `next_cursor` is returned on offset-paged responses too.\n\n```\nGET /features?type=bridge_clearances\u0026jurisdiction=TX\u0026limit=100\n  -\u003e { \"data\": [...], \"has_more\": true, \"next_cursor\": \"MjAyNi0...\" }\nGET /features?type=bridge_clearances\u0026jurisdiction=TX\u0026limit=100\u0026cursor=MjAyNi0...\n  -\u003e { \"data\": [...], \"has_more\": true, \"next_cursor\": \"MjAyNi0...\" }\n```\n\nStop when `has_more` is false; `next_cursor` is omitted on that last page.\n",
        "parameters": [
          {
            "description": "Feature type: cameras, rest_areas, weigh_stations, certified_scales, truck_repair, weather_stations, ev_charging, signs, road_conditions, etc. Required unless `group` is provided.",
            "in": "query",
            "name": "type",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Feature group id. Convenience expansion to all feature_types in the group. See GET /features/groups.\n\nWhen combined with `radius_km`, the radius is capped at 200 km — see that parameter.",
            "in": "query",
            "name": "group",
            "required": false,
            "schema": {
              "enum": [
                "imagery",
                "weather",
                "road_conditions",
                "traffic_performance",
                "planned_events",
                "alerts_advisories",
                "wildfires",
                "trucking",
                "traveler_services",
                "fuel_charging",
                "borders",
                "ferries",
                "transit",
                "tolls",
                "static_infrastructure",
                "operations"
              ],
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/jurisdiction"
          },
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          },
          {
            "$ref": "#/components/parameters/cursor"
          },
          {
            "$ref": "#/components/parameters/bbox"
          },
          {
            "$ref": "#/components/parameters/lat"
          },
          {
            "$ref": "#/components/parameters/lng"
          },
          {
            "$ref": "#/components/parameters/radius_km"
          },
          {
            "description": "Tri-state filter on `is_active`. `true` returns active only,\n`false` returns inactive (archived) only, omitted returns both.\n",
            "in": "query",
            "name": "active",
            "schema": {
              "enum": [
                "true",
                "false"
              ],
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/starts_after"
          },
          {
            "$ref": "#/components/parameters/starts_before"
          },
          {
            "$ref": "#/components/parameters/ends_after"
          },
          {
            "$ref": "#/components/parameters/ends_before"
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": [
                    {
                      "description": "Traffic camera - Highway 401 westbound at Yonge Street",
                      "direction": "westbound",
                      "feature_type": "cameras",
                      "has_details": true,
                      "id": "on-cam-401-yonge",
                      "is_active": true,
                      "jurisdiction": "ON",
                      "last_updated": "2026-03-29T14:50:00Z",
                      "latitude": 43.6532,
                      "longitude": -79.3832,
                      "name": "Hwy 401 at Yonge St",
                      "properties": {
                        "url": "https://511on.ca/cameras/cam_401_yonge.jpg",
                        "video_url": null
                      },
                      "road_name": "Highway 401",
                      "source": "on"
                    },
                    {
                      "description": "Fast-charging site, A12 eastbound",
                      "direction": "eastbound",
                      "feature_type": "ev_charging",
                      "has_details": true,
                      "id": "ndw-ev-a12-bunnik",
                      "is_active": true,
                      "jurisdiction": "NL",
                      "last_updated": "2026-09-07T08:22:00Z",
                      "latitude": 52.0662,
                      "longitude": 5.2003,
                      "name": "Fastned A12 Bunnik",
                      "properties": {
                        "availability_source": "nl_ocpi",
                        "availability_status": "available",
                        "availability_updated_at": "2026-09-07T08:22:00Z",
                        "available_points": 5,
                        "is_available": true,
                        "occupied_points": 2,
                        "operator": "Fastned",
                        "ports": 8,
                        "reported_points": 8,
                        "unavailable_points": 1
                      },
                      "road_name": "A12",
                      "source": "ndw"
                    },
                    {
                      "description": "RWIS weather station on I-75 near downtown Atlanta",
                      "direction": null,
                      "feature_type": "weather_stations",
                      "has_details": false,
                      "id": "ga-wx-i75-atl",
                      "is_active": true,
                      "jurisdiction": "GA",
                      "last_updated": "2026-03-29T14:45:00Z",
                      "latitude": 33.749,
                      "longitude": -84.388,
                      "name": "I-75 Atlanta Weather Station",
                      "properties": {
                        "humidity": 62,
                        "precipitation": "none",
                        "road_surface": "dry",
                        "temperature": 18.5,
                        "visibility": 16,
                        "wind_direction": "NW",
                        "wind_speed": 12.3
                      },
                      "road_name": "I-75",
                      "source": "ga"
                    }
                  ],
                  "has_more": false,
                  "limit": 100,
                  "offset": 0,
                  "total": 2
                },
                "schema": {
                  "$ref": "#/components/schemas/PagedFeatures"
                }
              }
            },
            "description": "Paginated features"
          },
          "400": {
            "content": {
              "application/json": {
                "example": {
                  "error": "type parameter is required"
                }
              }
            },
            "description": "Missing `type`, an `offset` above 50,000, a malformed `cursor`, or\n`cursor` and `offset` sent together.\n"
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "error": "jurisdiction parameter required on Free plan"
                }
              }
            },
            "description": "Plan limit exceeded"
          }
        },
        "summary": "List features",
        "tags": [
          "features"
        ]
      }
    },
    "/features/details/batch": {
      "post": {
        "description": "Fan-out variant of `/features/{id}/details` for callers that need to\nresolve many features at once. POST is used so the id list isn't\nconstrained by URL length.\n\nPer-id caching is identical to the single endpoint (Redis-backed\nkeyed on the id), so a batch that's a superset of previously\nrequested ids costs nothing extra for the overlap. Partial success\nis preserved: a single failed upstream fetch sets `cache: \"error\"`\nwith an `error` string on that entry without failing the whole\nrequest — or `cache: \"error-cached\"` when a recent failure was replayed\nfrom the negative cache instead of calling the source again. Missing\nids land in the response with `not_found: true`; ids the source has\nstopped listing come back with `cache: \"retired\"` and their last known\nproperties.\n\nA large batch against cold caches can run out of the request's time\nbudget before every id has been tried. The response array is positional,\nso the ids that were never attempted come back with\n`cache: \"skipped\"` and an `error` saying so, rather than as blanks. This\none is worth retrying — unlike `error-cached`, nothing has failed and a\nsmaller batch will resolve them. Entries already resolved in the same\nresponse are unaffected, and their detail is cached, so the retry only\npays for what is left.\n\nBounded by the plan's `max_batch_size`, and by a system ceiling of 500\nids per request that applies to every caller.\n",
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "ids": [
                  "on-cam-401-yonge",
                  "ga-cam-i75-249",
                  "missing-id"
                ]
              },
              "schema": {
                "properties": {
                  "ids": {
                    "items": {
                      "type": "string"
                    },
                    "minItems": 1,
                    "type": "array"
                  }
                },
                "required": [
                  "ids"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "count": 3,
                  "data": [
                    {
                      "cache": "hit",
                      "data": {
                        "feature_type": "cameras",
                        "id": "on-cam-401-yonge",
                        "jurisdiction": "ON",
                        "name": "Hwy 401 at Yonge St",
                        "properties": {
                          "url": "https://511on.ca/cameras/cam_401_yonge.jpg"
                        },
                        "source": "on"
                      },
                      "detail_available": true,
                      "id": "on-cam-401-yonge"
                    },
                    {
                      "cache": "miss",
                      "data": {
                        "feature_type": "cameras",
                        "id": "ga-cam-i75-249",
                        "jurisdiction": "GA",
                        "name": "I-75 at Exit 249",
                        "source": "ga"
                      },
                      "detail_available": true,
                      "id": "ga-cam-i75-249"
                    },
                    {
                      "cache": "",
                      "detail_available": false,
                      "id": "missing-id",
                      "not_found": true
                    }
                  ]
                }
              }
            },
            "description": "Per-id detail results (one entry per requested id, in order)"
          },
          "400": {
            "content": {
              "application/json": {
                "example": {
                  "error": "ids is required and must contain at least one non-empty id"
                }
              }
            },
            "description": "Invalid JSON body or empty ids array"
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "error": "batch too large: 150 ids requested, max 100 on the Pro plan — upgrade for more"
                }
              }
            },
            "description": "Batch exceeds plan's max_batch_size"
          }
        },
        "summary": "Feature detail batch (lazy-loaded)",
        "tags": [
          "features"
        ]
      }
    },
    "/features/geojson": {
      "get": {
        "description": "Returns features as a GeoJSON FeatureCollection. Requires Starter+ plan.",
        "parameters": [
          {
            "in": "query",
            "name": "type",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/jurisdiction"
          },
          {
            "$ref": "#/components/parameters/bbox"
          },
          {
            "description": "Defaults to active-only for map rendering. `active=false` drops the\nfilter and returns both active and inactive features (asymmetric vs\n`/features` for back-compat with map clients — use `/features?active=false`\nto fetch inactive-only).\n",
            "in": "query",
            "name": "active",
            "schema": {
              "enum": [
                "true",
                "false"
              ],
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/starts_after"
          },
          {
            "$ref": "#/components/parameters/starts_before"
          },
          {
            "$ref": "#/components/parameters/ends_after"
          },
          {
            "$ref": "#/components/parameters/ends_before"
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/geo+json": {
                "example": {
                  "features": [
                    {
                      "geometry": {
                        "coordinates": [
                          -79.3832,
                          43.6532
                        ],
                        "type": "Point"
                      },
                      "properties": {
                        "feature_type": "cameras",
                        "id": "on-cam-401-yonge",
                        "is_active": true,
                        "jurisdiction": "ON",
                        "name": "Hwy 401 at Yonge St",
                        "source": "on"
                      },
                      "type": "Feature"
                    }
                  ],
                  "type": "FeatureCollection"
                },
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "GeoJSON FeatureCollection"
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "error": "GeoJSON access requires Starter plan or higher"
                }
              }
            },
            "description": "GeoJSON not available on Free plan"
          }
        },
        "summary": "Features as GeoJSON",
        "tags": [
          "features"
        ]
      }
    },
    "/features/groups": {
      "get": {
        "description": "Returns the 16 feature-type groups with their member feature_types. Use the group `id` with `GET /features?group=\u003cid\u003e` to fetch all features in that group.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": [
                  {
                    "description": "Live traffic camera feeds",
                    "feature_types": [
                      "cameras"
                    ],
                    "id": "imagery",
                    "name": "Imagery",
                    "sort_order": 1
                  },
                  {
                    "description": "Weather stations, forecasts, alerts and warnings",
                    "feature_types": [
                      "weather_stations",
                      "regional_weather",
                      "weather_forecasts",
                      "weather_alerts",
                      "wind_warnings"
                    ],
                    "id": "weather",
                    "name": "Weather \u0026 Environment",
                    "sort_order": 2
                  },
                  {
                    "description": "Truck restrictions, routes, parking, inspections",
                    "feature_types": [
                      "truck_restrictions",
                      "weight_restrictions",
                      "bridge_clearances",
                      "truck_routes",
                      "freight_corridors",
                      "truck_parking",
                      "truck_rest_areas",
                      "weigh_stations",
                      "inspection_stations"
                    ],
                    "id": "trucking",
                    "name": "Trucking \u0026 Commercial Vehicles",
                    "sort_order": 8
                  }
                ]
              }
            },
            "description": "Feature group taxonomy"
          }
        },
        "summary": "List feature groups (taxonomy)",
        "tags": [
          "features"
        ]
      }
    },
    "/features/types": {
      "get": {
        "description": "Returns all feature types with total and active counts",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": [
                  {
                    "active_count": 11893,
                    "count": 12450,
                    "type": "cameras"
                  },
                  {
                    "active_count": 3142,
                    "count": 3200,
                    "type": "weather_stations"
                  },
                  {
                    "active_count": 4651,
                    "count": 4870,
                    "type": "signs"
                  },
                  {
                    "active_count": 1580,
                    "count": 1580,
                    "type": "rest_areas"
                  },
                  {
                    "active_count": 98500,
                    "count": 98500,
                    "type": "ev_charging"
                  },
                  {
                    "active_count": 45200,
                    "count": 45200,
                    "type": "bridge_clearances"
                  },
                  {
                    "active_count": 7200,
                    "count": 8900,
                    "type": "truck_restrictions"
                  },
                  {
                    "active_count": 1850,
                    "count": 2100,
                    "type": "road_conditions"
                  }
                ]
              }
            },
            "description": "Feature type counts"
          }
        },
        "summary": "List feature types",
        "tags": [
          "features"
        ]
      }
    },
    "/features/{id}/details": {
      "get": {
        "description": "Fetches detailed properties for a feature on demand. For parking-type\nfeatures (`truck_parking`, `truck_rest_areas`, `rest_areas`,\n`service_plazas`, `parking`) the response also carries\n`availability_trend[]` — the per-hour-of-day occupancy trend\n(`{ hour, avg_available, avg_capacity, samples, low_samples }`, hours in\nUTC) built from recorded live-occupancy history, so a client can answer\n\"is this lot typically full at my ETA?\". Empty/omitted until the time\nseries has accumulated for that feature.\n\nSome operators stop publishing a number once a lot is nearly full and\nreport only \"at or below this site's low threshold\" instead. Those\nobservations are counted in `low_samples` and are deliberately **excluded\nfrom `avg_available`** — there is no real number behind them, and\nsubstituting one (0, say) would assert the lot is *full*, a stronger\nclaim than the operator made. `avg_available` is therefore an average\nover the counted samples only, and is omitted entirely for an hour whose\nevery observation withheld its count.\n\n**Read the two together, and read them as counts of observations, not as\ntime.** Every bucket in this series is recorded *on change*, so a lot\nthat sits at its low threshold for three hours contributes one\n`low_samples` while a lot whose count jitters contributes many. A\nnon-zero `low_samples` therefore tells you the lot **reaches** its low\nthreshold in that hour — not what fraction of the hour it spends there —\nand an hour with a comfortable `avg_available` and a non-zero\n`low_samples` is one you should not read as reliably roomy. `samples` is\nthe honest confidence signal for the bucket either way.\n\n`low_samples` only accumulates for feeds that publish this state at all\n(currently the MAASTO TPIMS states in North America), and only from when\nthat capture began — a bucket can read `low_samples: 0` because the lot\nnever withheld a count, or because the trailing window still reaches\nback before the capture existed.\n\n`cache` reports where the detail came from: `hit` (cached upstream\ndetail), `miss` (fetched upstream on this request), `poll` (the source\nis enriched during polling, no on-demand fetch needed), `none` (no\ndetail loader exists for this source/resource), `retired` (the source\nstopped listing this id, so `is_active` is false and no upstream fetch\nis attempted — the properties returned are the last ones we saw while\nit was live), `error` (an upstream fetch was attempted and failed) or\n`error-cached` (a recent failure is still being served from the negative\ncache — no upstream call was made, so retrying immediately will not\nhelp). Failures are remembered for a short cooldown, and repeated\nsource-wide failures pause on-demand fetches for that source for a few\nminutes; both clear automatically. `retired` is not a failure and does\nnot clear: retrying it returns the same answer until the source lists\nthe id again.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "availability_trend": [
                    {
                      "avg_available": 18.4,
                      "avg_capacity": 40,
                      "hour": 11,
                      "low_samples": 0,
                      "samples": 62
                    },
                    {
                      "avg_available": 1.2,
                      "avg_capacity": 40,
                      "hour": 20,
                      "low_samples": 41,
                      "samples": 58
                    }
                  ],
                  "cache": "miss",
                  "data": {
                    "description": "Traffic camera - Highway 401 westbound at Yonge Street",
                    "direction": "westbound",
                    "feature_type": "cameras",
                    "id": "on-cam-401-yonge",
                    "is_active": true,
                    "jurisdiction": "ON",
                    "last_updated": "2026-03-29T14:50:00Z",
                    "latitude": 43.6532,
                    "longitude": -79.3832,
                    "name": "Hwy 401 at Yonge St",
                    "properties": {
                      "url": "https://511on.ca/cameras/cam_401_yonge.jpg",
                      "video_url": "https://511on.ca/cameras/cam_401_yonge.m3u8",
                      "views": [
                        {
                          "name": "Westbound",
                          "url": "https://511on.ca/cameras/cam_401_yonge_wb.jpg"
                        },
                        {
                          "name": "Eastbound",
                          "url": "https://511on.ca/cameras/cam_401_yonge_eb.jpg"
                        }
                      ]
                    },
                    "road_name": "Highway 401",
                    "source": "on"
                  },
                  "detail_available": true
                }
              }
            },
            "description": "Feature with full properties"
          },
          "404": {
            "content": {
              "application/json": {
                "example": {
                  "error": "feature not found"
                }
              }
            },
            "description": "Feature not found"
          }
        },
        "summary": "Feature detail (lazy-loaded)",
        "tags": [
          "features"
        ]
      }
    },
    "/fuel-prices": {
      "get": {
        "description": "Returns regional fuel prices. Weekly US state-level (EIA) and monthly Canadian\nprovince-level (StatCan). Use `latest=true` to get only the most recent observation\nper jurisdiction + fuel_type. 24-hour cache (data refreshes daily at the source).\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/jurisdiction"
          },
          {
            "description": "Filter by country",
            "in": "query",
            "name": "country",
            "schema": {
              "enum": [
                "US",
                "CA"
              ],
              "type": "string"
            }
          },
          {
            "description": "Filter by fuel grade",
            "in": "query",
            "name": "fuel_type",
            "schema": {
              "enum": [
                "regular",
                "midgrade",
                "premium",
                "diesel"
              ],
              "type": "string"
            }
          },
          {
            "description": "Start date (YYYY-MM-DD)",
            "in": "query",
            "name": "from",
            "schema": {
              "format": "date",
              "type": "string"
            }
          },
          {
            "description": "End date (YYYY-MM-DD)",
            "in": "query",
            "name": "to",
            "schema": {
              "format": "date",
              "type": "string"
            }
          },
          {
            "description": "Return only the most recent observation per jurisdiction + fuel_type",
            "in": "query",
            "name": "latest",
            "schema": {
              "default": false,
              "type": "boolean"
            }
          },
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": [
                    {
                      "country": "US",
                      "currency": "USD",
                      "frequency": "weekly",
                      "fuel_type": "regular",
                      "id": 12048,
                      "jurisdiction": "CA",
                      "period_date": "2026-05-05",
                      "price": 4.85,
                      "source": "EIA",
                      "unit": "gallon"
                    },
                    {
                      "country": "CA",
                      "currency": "CAD",
                      "frequency": "monthly",
                      "fuel_type": "regular",
                      "id": 12122,
                      "jurisdiction": "ON",
                      "period_date": "2026-04-01",
                      "price": 1.62,
                      "source": "StatCan",
                      "unit": "litre"
                    }
                  ],
                  "has_more": true,
                  "limit": 100,
                  "offset": 0,
                  "total": 312
                }
              }
            },
            "description": "Paginated fuel prices"
          }
        },
        "summary": "Regional fuel prices",
        "tags": [
          "fuel-prices"
        ]
      }
    },
    "/health": {
      "get": {
        "description": "Returns server status, version, uptime, and the combined live status\nof the Postgres and Redis dependencies in `db` (formatted as\n`\u003cpostgres\u003e/\u003credis\u003e`, each segment `ok` or `down`). Returns 200 when\nboth respond within 2 seconds; otherwise 503 with the same body shape.\n",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "db": "ok/ok",
                  "status": "ok",
                  "uptime": "2h30m",
                  "version": "0.1.0"
                },
                "schema": {
                  "$ref": "#/components/schemas/HealthResponse"
                }
              }
            },
            "description": "All dependencies healthy"
          },
          "503": {
            "content": {
              "application/json": {
                "example": {
                  "db": "ok/down",
                  "status": "degraded",
                  "uptime": "2h30m",
                  "version": "0.1.0"
                },
                "schema": {
                  "$ref": "#/components/schemas/HealthResponse"
                }
              }
            },
            "description": "One or more dependencies unhealthy"
          }
        },
        "security": [],
        "summary": "Health check",
        "tags": [
          "metadata"
        ]
      }
    },
    "/jurisdictions": {
      "get": {
        "description": "Public metadata — no API key required.\n\nReturns all active jurisdictions. By default, only real geographic\njurisdictions (US states + Canadian provinces, `scope=state`) are\nreturned — cross-cutting federal sources and sub-state regional\nfeeds are filtered out so customer pickers stay clean.\n\nUse `?scope=federal` to list cross-cutting federal sources (EIA,\nFHWA, WZDX_NPS), `?scope=regional` for sub-state feeds (511SF,\nWZDX_AUSTIN, ...), or `?scope=all` to return everything.\n\nEach row's `group_id` (when present) points at a record in\n`GET /jurisdictions/groups`: a state's primary feed plus its WZDx\nand sub-state siblings collapsed under one umbrella id.\n",
        "parameters": [
          {
            "description": "Filter by jurisdiction scope; default `state`",
            "in": "query",
            "name": "scope",
            "schema": {
              "default": "state",
              "enum": [
                "state",
                "federal",
                "regional",
                "all"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": [
                  {
                    "code": "ON",
                    "country": "CA",
                    "group_id": null,
                    "is_active": true,
                    "name": "Ontario",
                    "scope": "state"
                  },
                  {
                    "code": "GA",
                    "country": "US",
                    "group_id": null,
                    "is_active": true,
                    "name": "Georgia",
                    "scope": "state"
                  },
                  {
                    "code": "CA",
                    "country": "US",
                    "group_id": "us-ca",
                    "is_active": true,
                    "name": "California",
                    "scope": "state"
                  },
                  {
                    "code": "NY",
                    "country": "US",
                    "group_id": "us-ny",
                    "is_active": true,
                    "name": "New York",
                    "scope": "state"
                  }
                ],
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/Jurisdiction"
                  },
                  "type": "array"
                }
              }
            },
            "description": "Array of jurisdictions"
          }
        },
        "security": [],
        "summary": "List jurisdictions",
        "tags": [
          "metadata"
        ]
      }
    },
    "/jurisdictions/groups": {
      "get": {
        "description": "Public metadata — no API key required.\n\nReturns the canonical bundles for states whose feed is split across\nmultiple codes (state + WZDx + optional CWZ + optional sub-state\nregional). Querying `?jurisdiction=\u003cprimary_jurisdiction_code\u003e` on\n`/events` or `/features` is transparently expanded to all members.\n",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": [
                  {
                    "description": "California feeds (state + WZDx + 511SF Bay Area)",
                    "id": "us-ca",
                    "members": [
                      "CA",
                      "WZDX_CA",
                      "511SF"
                    ],
                    "name": "California",
                    "primary_jurisdiction_code": "CA",
                    "sort_order": 2
                  },
                  {
                    "description": "Colorado feeds (state + WZDx + CWZ)",
                    "id": "us-co",
                    "members": [
                      "CO",
                      "WZDX_CO",
                      "WZDX_CWZ_CO"
                    ],
                    "name": "Colorado",
                    "primary_jurisdiction_code": "CO",
                    "sort_order": 3
                  },
                  {
                    "description": "Texas feeds (state + WZDx + sub-state regional)",
                    "id": "us-tx",
                    "members": [
                      "TX",
                      "WZDX_TX",
                      "WZDX_AUSTIN"
                    ],
                    "name": "Texas",
                    "primary_jurisdiction_code": "TX",
                    "sort_order": 24
                  }
                ],
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/JurisdictionGroup"
                  },
                  "type": "array"
                }
              }
            },
            "description": "Array of jurisdiction groups"
          }
        },
        "security": [],
        "summary": "List jurisdiction groups",
        "tags": [
          "metadata"
        ]
      }
    },
    "/map/config": {
      "get": {
        "description": "Returns map limits config. No API key required. `detail_level` is\nalways `compact` (kept in the response for older clients).\n",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "detail_level": "compact",
                  "max_results": 100,
                  "rpm": 60
                }
              }
            },
            "description": "Map config"
          }
        },
        "security": [],
        "summary": "Map configuration",
        "tags": [
          "map"
        ]
      }
    },
    "/map/events": {
      "get": {
        "description": "Public, capped to MAP_MAX_RESULTS, no pagination (offset always 0).\nNo API key required.\n\nMap endpoints always return **active events only** — `status=archived`\nand `status=all` are silently clamped to `active`, and\n`start_time_from`/`start_time_to` are ignored for the analytics\ngate. Historical access requires an authenticated `/events`\ncall with an appropriate plan.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/jurisdiction"
          },
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/bbox"
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": [
                    {
                      "id": "on-evt-401-001",
                      "jurisdiction": "ON",
                      "last_updated": "2026-03-29T14:45:00Z",
                      "latitude": 43.65,
                      "longitude": -79.38,
                      "road_class": "interstate",
                      "severity": "major",
                      "source": "on",
                      "start_time": "2026-03-29T14:30:00Z",
                      "status": "active",
                      "title": "Multi-vehicle collision on Highway 401 WB",
                      "type": "incident"
                    }
                  ],
                  "has_more": false,
                  "limit": 100,
                  "offset": 0,
                  "total": 1
                }
              }
            },
            "description": "Paginated events"
          }
        },
        "security": [],
        "summary": "Map events",
        "tags": [
          "map"
        ]
      }
    },
    "/map/events/geojson": {
      "get": {
        "description": "Public, same restrictions as /map/events (capped, no pagination)",
        "responses": {
          "200": {
            "content": {
              "application/geo+json": {
                "example": {
                  "features": [
                    {
                      "geometry": {
                        "coordinates": [
                          -79.38,
                          43.65
                        ],
                        "type": "Point"
                      },
                      "properties": {
                        "id": "on-evt-401-001",
                        "jurisdiction": "ON",
                        "severity": "major",
                        "source": "on",
                        "title": "Multi-vehicle collision on Highway 401 WB",
                        "type": "incident"
                      },
                      "type": "Feature"
                    }
                  ],
                  "type": "FeatureCollection"
                }
              }
            },
            "description": "GeoJSON FeatureCollection"
          }
        },
        "security": [],
        "summary": "Map events GeoJSON",
        "tags": [
          "map"
        ]
      }
    },
    "/map/events/{id}": {
      "get": {
        "description": "Public compact detail for a single event — the map popup variant.\nNo API key required. Same shape as `/events/{id}` but with the\ncompact-list cut applied: `description` is replaced with a hint and\n`metadata` is omitted. Archived events beyond the anonymous 7-day\nwindow are rejected with 403; full detail requires an API key on\n`/events/{id}`.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "affected_roads": [
                    "Highway 401"
                  ],
                  "description": "Full event details available via the /api/v1/events endpoints",
                  "direction": "westbound",
                  "id": "on-evt-401-001",
                  "jurisdiction": "ON",
                  "last_updated": "2026-03-29T14:45:00Z",
                  "latitude": 43.65,
                  "longitude": -79.38,
                  "road_class": "interstate",
                  "severity": "major",
                  "source": "on",
                  "start_time": "2026-03-29T14:30:00Z",
                  "status": "active",
                  "title": "Multi-vehicle collision on Highway 401 WB near Yonge St",
                  "type": "incident"
                }
              }
            },
            "description": "Compact event detail"
          },
          "403": {
            "description": "Archived event beyond the anonymous window"
          },
          "404": {
            "description": "Event not found"
          }
        },
        "security": [],
        "summary": "Map event detail (compact)",
        "tags": [
          "map"
        ]
      }
    },
    "/map/features": {
      "get": {
        "description": "Public, capped to MAP_MAX_RESULTS, no pagination (offset always 0)",
        "parameters": [
          {
            "in": "query",
            "name": "type",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/jurisdiction"
          },
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/bbox"
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": [
                    {
                      "feature_type": "cameras",
                      "id": "on-cam-401-yonge",
                      "is_active": true,
                      "jurisdiction": "ON",
                      "last_updated": "2026-03-29T14:50:00Z",
                      "latitude": 43.6532,
                      "longitude": -79.3832,
                      "name": "Hwy 401 at Yonge St",
                      "source": "on"
                    }
                  ],
                  "has_more": false,
                  "limit": 100,
                  "offset": 0,
                  "total": 1
                }
              }
            },
            "description": "Paginated features (capped)"
          }
        },
        "security": [],
        "summary": "Map features",
        "tags": [
          "map"
        ]
      }
    },
    "/map/features/geojson": {
      "get": {
        "description": "Public, same restrictions as /map/features (capped, no pagination)",
        "responses": {
          "200": {
            "content": {
              "application/geo+json": {
                "example": {
                  "features": [
                    {
                      "geometry": {
                        "coordinates": [
                          -79.3832,
                          43.6532
                        ],
                        "type": "Point"
                      },
                      "properties": {
                        "feature_type": "cameras",
                        "id": "on-cam-401-yonge",
                        "is_active": true,
                        "jurisdiction": "ON",
                        "name": "Hwy 401 at Yonge St",
                        "source": "on"
                      },
                      "type": "Feature"
                    }
                  ],
                  "type": "FeatureCollection"
                }
              }
            },
            "description": "GeoJSON FeatureCollection"
          }
        },
        "security": [],
        "summary": "Map features GeoJSON",
        "tags": [
          "map"
        ]
      }
    },
    "/map/features/types": {
      "get": {
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": [
                  {
                    "active_count": 11893,
                    "count": 12450,
                    "type": "cameras"
                  },
                  {
                    "active_count": 3142,
                    "count": 3200,
                    "type": "weather_stations"
                  },
                  {
                    "active_count": 4651,
                    "count": 4870,
                    "type": "signs"
                  }
                ]
              }
            },
            "description": "Feature type counts"
          }
        },
        "security": [],
        "summary": "Map feature types",
        "tags": [
          "map"
        ]
      }
    },
    "/map/features/{id}/details": {
      "get": {
        "description": "Public single-feature detail — the map popup variant. No API key\nrequired. Cameras return full properties plus the lazy-loaded\nupstream detail (stream URLs are the public map's core UX); every\nother feature type returns the bare row with `properties` reduced\nto a `_detail` hint — full properties require an API key on\n`/features/{id}/details`.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "cache": "none",
                  "data": {
                    "feature_type": "truck_restrictions",
                    "id": "va-restr-1372",
                    "is_active": true,
                    "jurisdiction": "VA",
                    "latitude": 36.71,
                    "longitude": -76.24,
                    "name": "US-58 height restriction",
                    "properties": {
                      "_detail": "Full feature details available via the /api/v1/features endpoints"
                    },
                    "source": "va"
                  },
                  "detail_available": false
                }
              }
            },
            "description": "Feature detail (compact for non-camera types)"
          },
          "404": {
            "description": "Feature not found"
          }
        },
        "security": [],
        "summary": "Map feature detail (compact; cameras full)",
        "tags": [
          "map"
        ]
      }
    },
    "/plans": {
      "get": {
        "description": "Returns active subscription plans with their gating columns (RPM,\nrequest quotas, allow_* flags) **and** their admin-curated marketing\nbullets (`features`). The response is wrapped in a `plans` array;\nwhen Paddle is configured the response also carries\n`paddle_client_token` + `paddle_env` for the checkout overlay.\n\nResolution order for each feature on the frontend:\n`i18n_key` (when the current locale has it) → raw `label` → key.\n",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "paddle_client_token": "live_abc123",
                  "paddle_env": "live",
                  "plans": [
                    {
                      "allow_analytics": false,
                      "allow_geojson": false,
                      "allow_truck_data": false,
                      "code": "free",
                      "data_delay_sec": 900,
                      "description": "14-day trial with basic access",
                      "features": [
                        {
                          "icon": "check",
                          "id": 11,
                          "label": "60 RPM · 1k req/day",
                          "plan_id": 1,
                          "sort_order": 10
                        },
                        {
                          "icon": "check",
                          "id": 12,
                          "label": "2 jurisdictions",
                          "plan_id": 1,
                          "sort_order": 20
                        },
                        {
                          "i18n_key": "plan_feature.events_cameras",
                          "icon": "check",
                          "id": 13,
                          "plan_id": 1,
                          "sort_order": 30
                        },
                        {
                          "i18n_key": "plan_feature.email_support",
                          "icon": "check",
                          "id": 14,
                          "plan_id": 1,
                          "sort_order": 40
                        }
                      ],
                      "id": 1,
                      "is_active": true,
                      "max_jurisdictions": 2,
                      "max_keys": 1,
                      "max_requests_day": 1000,
                      "max_results_per_page": 100,
                      "name": "Free",
                      "rate_limit_rpm": 60,
                      "trial_days": 14
                    },
                    {
                      "allow_analytics": false,
                      "allow_geojson": true,
                      "allow_truck_data": false,
                      "code": "starter",
                      "data_delay_sec": 0,
                      "description": "For small apps and prototypes",
                      "features": [
                        {
                          "icon": "check",
                          "id": 21,
                          "label": "300 RPM · 50k req/day",
                          "plan_id": 2,
                          "sort_order": 10
                        },
                        {
                          "icon": "check",
                          "id": 22,
                          "label": "All 65 jurisdictions",
                          "plan_id": 2,
                          "sort_order": 20
                        },
                        {
                          "i18n_key": "plan_feature.geojson_export",
                          "icon": "check",
                          "id": 23,
                          "plan_id": 2,
                          "sort_order": 30
                        },
                        {
                          "icon": "check",
                          "id": 24,
                          "label": "500 results per page",
                          "plan_id": 2,
                          "sort_order": 40
                        }
                      ],
                      "id": 2,
                      "is_active": true,
                      "max_jurisdictions": 10,
                      "max_keys": 3,
                      "max_requests_day": 50000,
                      "max_results_per_page": 500,
                      "name": "Starter",
                      "rate_limit_rpm": 300,
                      "trial_days": 0
                    }
                  ]
                }
              }
            },
            "description": "Active plans + Paddle config"
          }
        },
        "security": [],
        "summary": "List subscription plans",
        "tags": [
          "metadata"
        ]
      }
    },
    "/routing/quota": {
      "get": {
        "description": "Returns the same `quota` block emitted alongside `/routing/route`\nresponses, without performing a routing call (zero quota charge).\nUsed by the portal's subscription card to render the usage bar +\nactive top-ups list.\n",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "reset_at": "2026-06-01T00:00:00Z",
                  "subscription": {
                    "limit": 7500,
                    "plan": "pro_7500",
                    "tier": "pro",
                    "used": 234
                  },
                  "topups": [
                    {
                      "calls_added": 1000,
                      "calls_remaining": 750,
                      "expires_at": "2026-05-31T23:59:59Z",
                      "id": "01HK...",
                      "pack_sku": "topup_routing_truck_1000",
                      "purchased_at": "2026-05-04T09:12:00Z",
                      "title": "Standard Pack"
                    }
                  ],
                  "total": {
                    "remaining": 8016
                  }
                }
              }
            },
            "description": "Quota envelope"
          }
        },
        "summary": "Probe the customer's current routing quota envelope",
        "tags": [
          "routing"
        ]
      }
    },
    "/routing/route": {
      "post": {
        "description": "Returns a routed polyline annotated with `warnings[]` — every nearby\nhazard, restriction, and operational note our database knows about\n(active incidents, planned construction, bridge clearance/weight\nproblems for the requested truck, public at-grade rail crossings\nwith 49 CFR 392.10 hazmat-stop awareness, truck/weight restrictions,\nweigh stations, weather, alerts, special events). When the load is\nhazardous, segments restricted to hazardous materials surface as\n`hazmat_restriction` warnings, and the routing engine is asked to avoid\nthem via the `hazardous`/`tunnel_category` profile. Invalid `hazardous`\nor `tunnel_category` values are rejected with HTTP 400.\n\nEvery successful call:\n  * Counts against the customer's monthly routing quota (subscription\n    bucket first, then top-ups FIFO by expiry). Cache hits still\n    charge — the route is the API call regardless of whether we\n    re-used a cached payload.\n  * Auto-saves the result to `saved_routes` with a 30-minute TTL.\n    The returned `route_id` can be refetched (with re-evaluated\n    warnings) via `GET /routing/route/saved/{id}` or persisted via\n    `POST /routing/route/saved/{id}/persist`.\n\nThe `truck` block is required on every request. See the request\nschema for the full set of vehicle parameters.\n\nUnknown request fields are accepted, not rejected — but any key the\nserver did not bind is echoed back in `ignored_fields` (sorted dotted\npaths, e.g. `truck.axles`) and in the `X-Routing-Ignored-Fields`\nresponse header. Use it to catch a mis-spelled field: `axles` instead of\n`axle_count`, or `origin.lon` instead of `origin.lng`, is silently\ndropped otherwise and the route is computed without it.\n\nSupplying `truck.hos` (the driver's Hours-of-Service clock) adds an\n`hos[]` channel to each route: the points where the driver must take a\nbreak or stop driving under the chosen regime (US, Canada, EU), each with\nreachable truck parking / rest areas before the limit. It is free on every\nplan. See `truck.hos` in the request schema.\n",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "alternatives": {
                    "maximum": 3,
                    "minimum": 0,
                    "type": "integer"
                  },
                  "arrival_time": {
                    "description": "Mutually exclusive with departure_time",
                    "type": "string"
                  },
                  "avoid": {
                    "items": {
                      "enum": [
                        "highway",
                        "toll_road",
                        "ferry",
                        "tunnel",
                        "dirt_road",
                        "car_shuttle_train",
                        "seasonal_closure"
                      ],
                      "type": "string"
                    },
                    "type": "array"
                  },
                  "avoid_areas": {
                    "description": "Hard-block routing through one or more geographic areas. Each entry\nsets `type` and the matching field group.\n",
                    "items": {
                      "properties": {
                        "east": {
                          "description": "bbox",
                          "type": "number"
                        },
                        "lat": {
                          "description": "circle centre",
                          "type": "number"
                        },
                        "lng": {
                          "description": "circle centre",
                          "type": "number"
                        },
                        "north": {
                          "description": "bbox",
                          "type": "number"
                        },
                        "points": {
                          "description": "polygon vertices as [lat, lng] pairs",
                          "items": {
                            "items": {
                              "type": "number"
                            },
                            "type": "array"
                          },
                          "type": "array"
                        },
                        "radius_m": {
                          "description": "circle radius in metres",
                          "type": "number"
                        },
                        "south": {
                          "description": "bbox",
                          "type": "number"
                        },
                        "type": {
                          "enum": [
                            "bbox",
                            "circle",
                            "polygon"
                          ],
                          "type": "string"
                        },
                        "west": {
                          "description": "bbox",
                          "type": "number"
                        }
                      },
                      "required": [
                        "type"
                      ],
                      "type": "object"
                    },
                    "type": "array"
                  },
                  "avoid_countries": {
                    "description": "Route around these countries entirely where possible (ISO 3166-1 alpha-3 codes, e.g. \"USA\", \"CAN\").",
                    "items": {
                      "type": "string"
                    },
                    "type": "array"
                  },
                  "avoid_truck_roads": {
                    "description": "Avoid these truck-road usage classes.",
                    "items": {
                      "enum": [
                        "through_traffic",
                        "through_routes"
                      ],
                      "type": "string"
                    },
                    "type": "array"
                  },
                  "avoid_zones": {
                    "description": "Avoid whole categories of zone along the route.",
                    "items": {
                      "enum": [
                        "environmental",
                        "congestion_pricing"
                      ],
                      "type": "string"
                    },
                    "type": "array"
                  },
                  "cargo": {
                    "description": "Cargo metadata. Not proxied to the routing engine; stored on the saved route\nand available to future enrichment rules.\n",
                    "properties": {
                      "hazmat_class": {
                        "description": "DOT placard class (e.g., \"1.3D\", \"3\", \"8\"). Finer-grained than\nthe truck.hazardous bucket; future enrichment rules can use the\nclass digit for stricter 49 CFR 392.10 rail-crossing logic.\n",
                        "type": "string"
                      }
                    },
                    "type": "object"
                  },
                  "currency": {
                    "description": "ISO 4217; only honored when tolls is requested",
                    "type": "string"
                  },
                  "customer_route_id": {
                    "description": "Opaque client-side correlation key. Echoed verbatim in the response.\nNot used by enrichment.\n",
                    "maxLength": 128,
                    "type": "string"
                  },
                  "departure_time": {
                    "description": "ISO 8601 or 'now'; defaults to 'now'",
                    "type": "string"
                  },
                  "destination": {
                    "properties": {
                      "lat": {
                        "type": "number"
                      },
                      "lng": {
                        "type": "number"
                      }
                    },
                    "required": [
                      "lat",
                      "lng"
                    ],
                    "type": "object"
                  },
                  "details": {
                    "description": "Per-span road-attribute detail channels to compute along the route\n(returned on the route's spans). Independent of `include`.\n",
                    "items": {
                      "enum": [
                        "speed_limit",
                        "dynamic_speed",
                        "functional_class",
                        "names",
                        "route_numbers",
                        "country",
                        "lanes",
                        "notices",
                        "restrictions",
                        "truck_attributes",
                        "toll_systems"
                      ],
                      "type": "string"
                    },
                    "type": "array"
                  },
                  "enrichment": {
                    "description": "Per-request tuning for the post-routing truck-aware filter pipeline.\nNone of these fields are sent to the routing engine — they shape what\n`warnings[]` contains and, via `include_features`, what\n`features[]` contains. Omit the block to keep defaults.\n",
                    "properties": {
                      "buffer_m": {
                        "default": 100,
                        "description": "Spatial-join buffer radius around the polyline, in metres.",
                        "maximum": 1000,
                        "minimum": 10,
                        "type": "number"
                      },
                      "clearance_pad_m": {
                        "default": 0.15,
                        "description": "Safety pad added to `truck.height_m` when evaluating\nbridge_clearances. A warning fires when\n`clearance_m \u003c truck.height_m + clearance_pad_m`. The same\npad governs the on-deck overhead check that emits\n`deck_clearance` warnings (NBI item 53).\n",
                        "maximum": 1,
                        "minimum": 0,
                        "type": "number"
                      },
                      "exclude_types": {
                        "description": "Drop these feature_types from the spatial-join entirely. Useful\nfor trimming the response when a UI doesn't display, e.g., weigh\nstations.\n",
                        "items": {
                          "enum": [
                            "future_construction",
                            "bridge_clearances",
                            "tunnel_clearances",
                            "rail_crossings",
                            "truck_restrictions",
                            "weight_restrictions",
                            "weigh_stations",
                            "inspection_stations",
                            "alerts",
                            "special_events",
                            "weather_stations",
                            "border_crossings",
                            "chain_controls"
                          ],
                          "type": "string"
                        },
                        "type": "array"
                      },
                      "include_features": {
                        "description": "Opt the response into the `features[]` channel — amenity/POI\nmatches along the route (truck parking, rest areas, service\nplazas, EV charging), distinct from hazard `warnings[]`. Each\nentry is a POI feature_type; `rest_areas` are surfaced only\nwhen truck-friendly. Parking/rest features carry\n`available_spots`, `capacity`, and `as_of` (the count's\nobservation time) as first-class fields when the source feed\npublishes live occupancy (omitted when it doesn't;\n`available_spots: 0` means known full). A per-hour occupancy\n*trend* for a specific lot is available on demand from\n`GET /features/{id}/details` (`availability_trend[]`). Omitted/empty =\nno `features[]` (the default — no added cost).\n\n**Plan-gated by count.** Each plan caps how many distinct\nfeature types you may select at once (Free: none; the cap\nrises with tier; Enterprise: unlimited). Selecting more types\nthan your plan allows returns `403 feature_types_limit`, and a\nplan with no allowance returns `403 feature_channel_unavailable`.\nYour current cap is published as `routing_max_feature_types` on\n`GET /customer/plans`.\n",
                        "items": {
                          "enum": [
                            "truck_parking",
                            "truck_rest_areas",
                            "rest_areas",
                            "service_plazas",
                            "ev_charging",
                            "certified_scales",
                            "truck_repair",
                            "truck_services",
                            "truck_washes"
                          ],
                          "type": "string"
                        },
                        "type": "array"
                      },
                      "max_distance_m": {
                        "description": "Only emit warnings within this distance from the route origin.\n0 (default) = unlimited. Also bounds features[] when set.\n",
                        "minimum": 0,
                        "type": "number"
                      },
                      "min_severity": {
                        "description": "Filter out warnings below this threshold. Ranking is\ninfo \u003c warning \u003c critical. Critical warnings (e.g., bridge\nclearance violations) always pass. Weigh/inspection stations\ncarry an operational `status` (`open`/`closed`/`unknown`)\nwith an `as_of` observation time — when we last confirmed\nthe station in an upstream poll. **Read `as_of`; do not\nassume a fixed freshness.** Poll cadence is per source and\nvaries widely (hourly for some, twice a day for others), so\nan `as_of` several hours old is normal and not a fault.\nOnly a small minority of stations carry a status at all —\non a route through a jurisdiction whose feed publishes\nnone, a status filter correctly returns nothing. An **open** station is\n`warning` severity (you must pull in), while closed/unknown\nstay `info` — so `min_severity: \"warning\"` surfaces only\nstations you must stop at. `border_crossing` warnings (live\nland-border commercial waits on a cross-border route — CBP\nand CBSA in North America, the єЧерга electronic queue on\nUkraine's EU frontier) carry a severity keyed on the\ncommercial-lane wait: `info` under 15 min, `warning` from\n15 min, `critical` past 45 min or when the port is closed.\n`chain_control` warnings carry a winter chain /\ntraction law in effect on the route. Severity follows\nhow much the control binds a **truck**, not how bad the\nweather is: `info` for an advisory (chains advised or to\nbe carried), `warning` when chains are required on\ncommercial vehicles, and `critical` when they are\nrequired on all vehicles with no exemption at all\n(California R-3), which accompanies conditions at the\nedge of a closure. `description` carries the road\nauthority's own wording, prefixed with its own code\nwhere it publishes one — California's R-1/R-2\nexemptions are written for passenger vehicles and do\n**not** release a commercial vehicle, so both map to\n`warning`. `as_of` reports when we last confirmed the\nreading, which matters because these flip within\nminutes on a storm front.\n\nEvery feed that reports a wait sets\n`properties.commercial_wait_minutes` — always minutes, even\nwhere the human-readable `description` renders a multi-day\nqueue as days and hours. **When a feed publishes no wait for\na crossing, the field is absent — never `0`.** Several\nagencies encode \"we do not report this\" as a literal `--`,\n`N/A` or `Not Applicable` in the wait column, and those are\nparsed as *no reading*, not as a clear border; the warning\nstill appears (so the crossing is on your route) at `info`\nseverity with a description that claims no wait. Treat a\nmissing `commercial_wait_minutes` as unknown: `0` is a\npublished zero and means the agency reports no delay. Feed-specific extras: the CBP feed\nadds a `fast_lane_wait_minutes` / `fast_lane_savings_minutes`\npair (the expedited-lane edge for FAST-enrolled carriers)\nand a `wait_trend` of `rising` / `falling` / `steady` (this\npoll's commercial wait versus the previous one); єЧерга adds\n`vehicles_in_queue` (trucks currently queued). Note єЧерга\npublishes each checkpoint once per vehicle type — only the\ntruck queue drives severity, so a long coach queue never\nbands a freight route.\n",
                        "enum": [
                          "info",
                          "warning",
                          "critical"
                        ],
                        "type": "string"
                      },
                      "provenance": {
                        "description": "Restrict `warnings[]` and `features[]` to observations of the\nlisted classes. Omitted/empty = every class (the default).\n\n  * `authoritative` — a road authority or infrastructure\n    operator published it: a state DOT / 511 system, a national\n    access point, a police CAD feed, a motorway concessionaire.\n    This is the great majority of what we serve.\n  * `commercial` — a commercial traffic-data vendor's probe or\n    telematics observation, republished by the authority.\n  * `crowdsourced` — reported or maintained by end users.\n\n`[\"authoritative\"]` is the DOT-only view: use it when a\ndispatcher must be able to say a lane closure came from the\nroad authority and not from another driver's phone. Filtering\nhappens in the spatial query, so an excluded observation never\noccupies one of the response's warning slots.\n\nEvery warning and feature reports its own class in the\n`provenance` field, so you can also decide per record instead\nof filtering. The specific upstream feed behind a record is\nnot disclosed.\n",
                        "items": {
                          "enum": [
                            "authoritative",
                            "commercial",
                            "crowdsourced"
                          ],
                          "type": "string"
                        },
                        "type": "array"
                      },
                      "skip_temporal_filter": {
                        "default": false,
                        "description": "When true, keeps traffic events whose end_time is already past\nprojected arrival. Useful for planning-ahead runs.\n",
                        "type": "boolean"
                      }
                    },
                    "type": "object"
                  },
                  "exclude_countries": {
                    "description": "Hard-exclude these countries from the route (ISO 3166-1 alpha-3). Stricter than avoid_countries — the route will fail rather than pass through.",
                    "items": {
                      "type": "string"
                    },
                    "type": "array"
                  },
                  "include": {
                    "description": "Optional route detail channels to compute. `polyline` and `summary`\nare always included. Request `tolls` to have the routing engine\nprice the route's toll systems for your truck profile — the cost\nthen appears as `routes[].sections[].tolls[]` (per toll system)\nand is rolled up per currency into `routes[].summary.toll_costs[]`.\nSet `currency` (ISO 4217) to control the fare currency. Toll\npricing adds upstream cost, so it is off unless requested.\n\nRequest `mileage_by_state` for a per-jurisdiction distance\nbreakdown on `routes[].summary.mileage_by_state[]` — one entry\nper US state / Canadian province the route touches, ordered\nlongest-first, computed by intersecting the route with fixed\njurisdiction boundary polygons. A stable, reproducible figure\nfor IFTA quarterly reporting, mileage-based billing, and driver\npay. Off unless requested (it adds a spatial query).\n\nRequest `toll_alternative` (paid plans only; implies `tolls`)\nto also compute a second, toll-free route and price the trade —\nextra distance/time vs. toll dollars saved — returned as the\ntop-level `toll_alternative` block. Because this computes a\nsecond route, **it is billed as 2 routing calls** (or 1 when the\nwhole response is served from cache). If the second route can't\nbe produced the primary route still returns, with the reason in\n`toll_alternative_omitted`.\n\nRequest `parking_trend` to add `typical_occupancy` to every\nparking/rest feature in `features[]` and to each\n`hos[].suggested_stops[]` — what that lot's recorded occupancy\nhistory says about the hour you are projected to reach it,\nrather than how many spaces are free right now. Off unless\nrequested (it adds a history query), and a lot with no\nrecorded history simply omits the block.\n",
                    "items": {
                      "enum": [
                        "polyline",
                        "summary",
                        "instructions",
                        "actions",
                        "tolls",
                        "incidents",
                        "routing_zones",
                        "elevation",
                        "route_handle",
                        "section_fares",
                        "mileage_by_state",
                        "toll_alternative",
                        "parking_trend"
                      ],
                      "type": "string"
                    },
                    "type": "array"
                  },
                  "language": {
                    "description": "BCP-47 (e.g. en-US)",
                    "type": "string"
                  },
                  "optimize": {
                    "default": "time",
                    "enum": [
                      "time",
                      "distance"
                    ],
                    "type": "string"
                  },
                  "origin": {
                    "properties": {
                      "lat": {
                        "type": "number"
                      },
                      "lng": {
                        "type": "number"
                      }
                    },
                    "required": [
                      "lat",
                      "lng"
                    ],
                    "type": "object"
                  },
                  "tags": {
                    "description": "Free-form labels for fleet/lane analytics rollup. Echoed and stored\non the saved route. Max 16 tags, each up to 64 chars.\n",
                    "items": {
                      "maxLength": 64,
                      "type": "string"
                    },
                    "maxItems": 16,
                    "type": "array"
                  },
                  "traffic": {
                    "default": "live",
                    "enum": [
                      "live",
                      "historical",
                      "off"
                    ],
                    "type": "string"
                  },
                  "truck": {
                    "description": "Required block (the endpoint is truck-only).",
                    "properties": {
                      "axle_count": {
                        "type": "integer"
                      },
                      "axle_group_weight_t": {
                        "description": "Per-axle-group weight caps in tonnes (mapped to the router's weight-per-axle-group profile).",
                        "properties": {
                          "single": {
                            "type": "number"
                          },
                          "tandem": {
                            "type": "number"
                          },
                          "triple": {
                            "type": "number"
                          }
                        },
                        "type": "object"
                      },
                      "axle_weight_t": {
                        "type": "number"
                      },
                      "commercial": {
                        "type": "boolean"
                      },
                      "fuel": {
                        "enum": [
                          "diesel",
                          "gasoline",
                          "lpg",
                          "cng"
                        ],
                        "type": "string"
                      },
                      "hazardous": {
                        "items": {
                          "enum": [
                            "explosive",
                            "gas",
                            "flammable",
                            "combustible",
                            "organic",
                            "poison",
                            "radioactive",
                            "corrosive",
                            "poison_inhalation",
                            "harmful_to_water",
                            "other"
                          ],
                          "type": "string"
                        },
                        "type": "array"
                      },
                      "height_m": {
                        "maximum": 5,
                        "type": "number"
                      },
                      "hos": {
                        "description": "Driver's Hours-of-Service clock at departure. When present (with\na `ruleset`), the response gains an `hos[]` projection of where\nthe driver must take a break or stop driving under that regime,\neach annotated with reachable truck parking / rest areas before\nthe limit. Where a parking feed publishes live occupancy, the\nreachable-parking suggestions are ordered by open spaces and a\nstop is flagged `feasible: false` when every reachable lot is\nreported full. The response also gains `routes[].hos_summary`\nwith the arrival time once mandated rests are inserted, the delay\nthey add, and — when you sent `arrival_time` — whether the trip\nstill meets that window. **Free on every plan** — no quota beyond\nthe routing call itself. Not proxied to the routing engine.\n\nA reusable truck profile should store only `ruleset` (your\nfleet's governing regime); supply the per-trip `*_remaining_s`\ncounters inline — they win over the stored profile per field.\nEach counter is *seconds remaining* against the ruleset's limit;\nomit a counter to assume a fresh driver (the full limit).\n",
                        "properties": {
                          "cycle_remaining_s": {
                            "description": "Seconds left in the weekly/cycle budget.",
                            "minimum": 0,
                            "type": "integer"
                          },
                          "drive_remaining_s": {
                            "description": "Seconds left on the driving limit.",
                            "minimum": 0,
                            "type": "integer"
                          },
                          "duty_remaining_s": {
                            "description": "Seconds left in the on-duty window (US/Canada).",
                            "minimum": 0,
                            "type": "integer"
                          },
                          "elapsed_remaining_s": {
                            "description": "Seconds left in the elapsed (wall-clock) window (Canada).",
                            "minimum": 0,
                            "type": "integer"
                          },
                          "ruleset": {
                            "description": "Hours-of-Service regime. Omit to default from the deployment\nregion (NA → `us`, EU → `eu`). Canadian operators must set\n`canada_south` (≤60°N) or `canada_north` (\u003e60°N\nextended-limits tier) explicitly. (`aetr` is planned.)\n",
                            "enum": [
                              "us",
                              "canada_south",
                              "canada_north",
                              "eu"
                            ],
                            "type": "string"
                          },
                          "since_break_s": {
                            "description": "Seconds of driving since the last qualifying break.",
                            "minimum": 0,
                            "type": "integer"
                          }
                        },
                        "type": "object"
                      },
                      "length_m": {
                        "maximum": 30,
                        "type": "number"
                      },
                      "max_speed_kph": {
                        "type": "integer"
                      },
                      "occupancy": {
                        "type": "integer"
                      },
                      "permit_number": {
                        "description": "Oversize/overweight permit identifier carried by this truck.\nNot proxied to the routing engine — stored on the saved route and echoed in\nthe response for client-side correlation.\n",
                        "type": "string"
                      },
                      "profile": {
                        "default": "tractor",
                        "enum": [
                          "tractor",
                          "straight",
                          "box"
                        ],
                        "type": "string"
                      },
                      "profile_id": {
                        "description": "ID of a reusable truck profile from your fleet library\n(managed under the developer portal). When set, the server\nloads that profile's stored truck block and deep-merges any\ninline `truck.*` fields supplied alongside it on top — inline\nvalues win per field. The fully-resolved block is echoed back\nas `resolved_truck` so you can audit exactly what was applied.\nA 400 (`profile_not_found`) is returned if the ID isn't one of\nyour profiles.\n",
                        "type": "string"
                      },
                      "trailer_axle_count": {
                        "type": "integer"
                      },
                      "trailer_count": {
                        "type": "integer"
                      },
                      "tunnel_category": {
                        "enum": [
                          "B",
                          "C",
                          "D",
                          "E"
                        ],
                        "type": "string"
                      },
                      "weight_t": {
                        "description": "Gross combined weight in tonnes",
                        "type": "number"
                      },
                      "width_m": {
                        "maximum": 4.5,
                        "type": "number"
                      }
                    },
                    "required": [],
                    "type": "object"
                  },
                  "units": {
                    "default": "metric",
                    "enum": [
                      "metric",
                      "imperial"
                    ],
                    "type": "string"
                  },
                  "waypoints": {
                    "items": {
                      "properties": {
                        "lat": {
                          "type": "number"
                        },
                        "lng": {
                          "type": "number"
                        },
                        "stop_over": {
                          "type": "boolean"
                        }
                      },
                      "type": "object"
                    },
                    "type": "array"
                  }
                },
                "required": [
                  "origin",
                  "destination",
                  "truck"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "cached": false,
                  "customer_route_id": "my-route-42",
                  "expires_at": "2026-05-18T13:30:00Z",
                  "ignored_fields": [
                    "truck.axles"
                  ],
                  "is_persisted": false,
                  "quota": {
                    "reset_at": "2026-06-01T00:00:00Z",
                    "subscription": {
                      "limit": 7500,
                      "plan": "pro_7500",
                      "tier": "pro",
                      "used": 2
                    },
                    "topups": [],
                    "total": {
                      "remaining": 7498
                    }
                  },
                  "resolved_truck": {
                    "axle_count": 5,
                    "fuel": "diesel",
                    "height_m": 4.1,
                    "profile_id": "9f1c2e7a-6b3d-4a21-9f0e-2c5d8b7a1e34",
                    "weight_t": 36
                  },
                  "route_id": "01HKZ9P5XJVQ6NB8E2RTH4FMW7",
                  "routes": [
                    {
                      "features": [
                        {
                          "as_of": "2026-05-18T10:55:00Z",
                          "available_spots": 18,
                          "capacity": 40,
                          "distance_along_route_m": 142000,
                          "geometry": {
                            "coordinates": [
                              -77.6,
                              44.1
                            ],
                            "type": "Point"
                          },
                          "name": "ONroute Trenton North",
                          "properties": {
                            "amenities": [
                              "restrooms",
                              "fuel"
                            ],
                            "spaces": 40
                          },
                          "provenance": "authoritative",
                          "source": "ON 511",
                          "source_id": "feat_truck_parking_on_4821",
                          "type": "truck_parking",
                          "typical_occupancy": {
                            "avg_available": 4.2,
                            "avg_capacity": 40,
                            "hour": 21,
                            "low_samples": 11,
                            "samples": 58
                          }
                        }
                      ],
                      "geometry": {
                        "coordinates": [
                          [
                            -74.006,
                            40.7128
                          ]
                        ],
                        "type": "LineString"
                      },
                      "hos": [
                        {
                          "distance_along_route_m": 720000,
                          "feasible": true,
                          "legal_deadline": "2026-05-18T14:00:00Z",
                          "projected_time": "2026-05-18T14:00:00Z",
                          "reason": "break_required",
                          "suggested_stops": [
                            {
                              "distance_along_route_m": 705000,
                              "geometry": {
                                "coordinates": [
                                  -93.1,
                                  41.6
                                ],
                                "type": "Point"
                              },
                              "name": "I-80 Rest Area MP 284",
                              "properties": {
                                "truck_parking": 24
                              },
                              "provenance": "authoritative",
                              "source": "IA 511",
                              "source_id": "feat_rest_areas_ia_284",
                              "type": "rest_area",
                              "typical_occupancy": {
                                "avg_available": 9.6,
                                "avg_capacity": 24,
                                "hour": 13,
                                "low_samples": 0,
                                "samples": 41
                              }
                            }
                          ]
                        },
                        {
                          "distance_along_route_m": 990000,
                          "feasible": false,
                          "legal_deadline": "2026-05-18T17:30:00Z",
                          "projected_time": "2026-05-18T17:30:00Z",
                          "reason": "drive_limit",
                          "suggested_stops": []
                        }
                      ],
                      "hos_summary": {
                        "adjusted_arrival_time": "2026-05-19T04:00:00Z",
                        "delay_s": 37800,
                        "meets_arrival_window": false
                      },
                      "notices": [
                        {
                          "code": "violatedVehicleRestriction",
                          "title": "Route violates a posted truck restriction near the destination"
                        }
                      ],
                      "sections": [
                        {
                          "distance_m": 120000,
                          "duration_s": 4200,
                          "summary": "I-95 N",
                          "tolls": [
                            {
                              "fares": [
                                {
                                  "name": "Class 5",
                                  "price": {
                                    "currency": "USD",
                                    "value": 12.5
                                  }
                                }
                              ],
                              "system": "New Jersey Turnpike"
                            }
                          ]
                        }
                      ],
                      "summary": {
                        "distance_m": 350000,
                        "duration_s": 12600,
                        "mileage_by_state": [
                          {
                            "country": "US",
                            "distance_m": 180000,
                            "name": "Pennsylvania",
                            "state": "PA"
                          },
                          {
                            "country": "US",
                            "distance_m": 95000,
                            "name": "New Jersey",
                            "state": "NJ"
                          },
                          {
                            "country": "US",
                            "distance_m": 75000,
                            "name": "New York",
                            "state": "NY"
                          }
                        ],
                        "toll_costs": [
                          {
                            "currency": "USD",
                            "value": 25
                          }
                        ]
                      },
                      "warnings": [
                        {
                          "description": "I-95 NB overpass at MP 23: 4.0m clearance; your truck is 4.2m.",
                          "distance_along_route_m": 12450,
                          "geometry": {
                            "coordinates": [
                              -74.006,
                              40.7128
                            ],
                            "type": "Point"
                          },
                          "projected_arrival_time": "2026-05-18T11:12:00Z",
                          "properties": {
                            "clearance_m": 4,
                            "truck_height_m": 4.2
                          },
                          "provenance": "authoritative",
                          "severity": "critical",
                          "source": "FHWA NBI",
                          "source_id": "feat_bridge_clearances_nbi_12345",
                          "title": "Bridge clearance below truck height",
                          "type": "bridge_clearance"
                        },
                        {
                          "description": "US-1 through-truss bridge: deck clearance 4.1m; your truck is 4.2m.",
                          "distance_along_route_m": 21800,
                          "geometry": {
                            "coordinates": [
                              -74.01,
                              40.72
                            ],
                            "type": "Point"
                          },
                          "projected_arrival_time": "2026-05-18T11:24:00Z",
                          "properties": {
                            "deck_clearance_m": 4.1,
                            "truck_height_m": 4.2
                          },
                          "severity": "critical",
                          "source": "FHWA NBI",
                          "source_id": "feat_bridge_clearances_nbi_67890",
                          "title": "Overhead clearance on bridge deck below truck height",
                          "type": "deck_clearance"
                        },
                        {
                          "description": "Wallace Tunnel clearance 4.11m; your truck is 4.2m.",
                          "distance_along_route_m": 30500,
                          "geometry": {
                            "coordinates": [
                              -88.03,
                              30.69
                            ],
                            "type": "Point"
                          },
                          "projected_arrival_time": "2026-05-18T11:36:00Z",
                          "properties": {
                            "min_vertical_clearance_m": 4.11,
                            "truck_height_m": 4.2
                          },
                          "severity": "critical",
                          "source": "FHWA NTI",
                          "source_id": "feat_tunnel_clearances_nti_AL-000123",
                          "title": "Tunnel clearance below truck height",
                          "type": "tunnel_clearance"
                        },
                        {
                          "as_of": "2026-05-18T10:58:00Z",
                          "description": "I-85 SB weigh station",
                          "distance_along_route_m": 48200,
                          "geometry": {
                            "coordinates": [
                              -84.1,
                              33.6
                            ],
                            "type": "Point"
                          },
                          "projected_arrival_time": "2026-05-18T11:40:00Z",
                          "properties": {
                            "is_open": true,
                            "status": "open"
                          },
                          "provenance": "authoritative",
                          "severity": "warning",
                          "source": "GA 511",
                          "source_id": "feat_weigh_stations_ga_ws-1",
                          "status": "open",
                          "title": "Weigh station ahead — open",
                          "type": "weigh_station"
                        },
                        {
                          "as_of": "2026-05-18T10:55:00Z",
                          "description": "Commercial border wait ~50 min. FAST lane ~35 min faster. Wait is rising.",
                          "distance_along_route_m": 512000,
                          "geometry": {
                            "coordinates": [
                              -75.95,
                              44.34
                            ],
                            "type": "Point"
                          },
                          "projected_arrival_time": "2026-05-18T18:20:00Z",
                          "properties": {
                            "border": "canada",
                            "commercial": {
                              "delay_minutes": 50,
                              "fast_delay_minutes": 15,
                              "status": "delay",
                              "trend": "rising"
                            },
                            "commercial_wait_minutes": 50,
                            "fast_lane_savings_minutes": 35,
                            "fast_lane_wait_minutes": 15,
                            "passenger": {
                              "delay_minutes": 5
                            },
                            "wait_trend": "rising"
                          },
                          "severity": "critical",
                          "source": "CBP",
                          "source_id": "feat_border_crossings_CBP-bwt-070801",
                          "status": "open",
                          "title": "Alexandria Bay",
                          "type": "border_crossing"
                        },
                        {
                          "as_of": "2026-07-16T09:40:00Z",
                          "description": "Commercial border wait ~4 d 3 h. 1006 trucks in queue.",
                          "distance_along_route_m": 288000,
                          "geometry": {
                            "coordinates": [
                              23.8105033,
                              51.1880897
                            ],
                            "type": "Point"
                          },
                          "projected_arrival_time": "2026-07-16T14:05:00Z",
                          "properties": {
                            "attribution": "Дані: єЧерга (Держмитслужба / Мінрозвитку), CC BY 4.0",
                            "commercial_wait_minutes": 5964,
                            "is_paused": false,
                            "neighbor_country": "Польща",
                            "queue_type": "dynamic",
                            "vehicle_type": "truck",
                            "vehicles_in_queue": 1006,
                            "wait_time_minutes": 5964,
                            "wait_time_seconds": 357840
                          },
                          "provenance": "authoritative",
                          "severity": "critical",
                          "source": "UKR",
                          "source_id": "1",
                          "status": "open",
                          "title": "Ягодин – Дорогуськ (для вантажівок ≥ 7,5 тонн)",
                          "type": "border_crossing"
                        }
                      ]
                    }
                  ],
                  "tags": [
                    "fleet-east",
                    "lane-LA-NYC"
                  ],
                  "toll_alternative": {
                    "cached": false,
                    "extra_distance_m": 22000,
                    "extra_duration_s": 1200,
                    "geometry": {
                      "coordinates": [
                        [
                          -74.01,
                          40.71
                        ]
                      ],
                      "type": "LineString"
                    },
                    "savings": [
                      {
                        "currency": "USD",
                        "value": 25
                      }
                    ],
                    "summary": {
                      "distance_m": 372000,
                      "duration_s": 13800,
                      "toll_costs": []
                    }
                  }
                }
              }
            },
            "description": "Route computed with warnings enriched",
            "headers": {
              "X-Routing-RateLimit-Limit": {
                "description": "Your plan's routing-only per-minute cap — a separate bucket from\nthe data-API rate limit, so a burst of routing calls does not\nconsume your data allowance or vice versa. The value varies by\nplan and is subject to change: read it here rather than\nhardcoding one. Omitted when the plan defines no routing-specific\ncap (the data-API limit then applies).\n",
                "schema": {
                  "type": "integer"
                }
              },
              "X-Routing-RateLimit-Remaining": {
                "description": "Routing calls still available in the current minute window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-Routing-RateLimit-Reset": {
                "description": "Unix epoch seconds at which the current window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "400": {
            "content": {
              "application/json": {
                "examples": {
                  "conflicting_times": {
                    "value": {
                      "code": "conflicting_times",
                      "message": "departure_time and arrival_time are mutually exclusive"
                    }
                  },
                  "invalid_avoid_feature": {
                    "value": {
                      "code": "invalid_avoid_feature",
                      "message": "avoid: \"tolls\" is not valid; accepted: car_shuttle_train, dirt_road, ferry, highway, seasonal_closure, toll_road, tunnel"
                    }
                  },
                  "invalid_body": {
                    "value": {
                      "code": "invalid_body",
                      "message": "request body is not valid JSON"
                    }
                  },
                  "invalid_enrichment_severity": {
                    "value": {
                      "code": "invalid_enrichment_severity",
                      "message": "enrichment.min_severity must be info, warning, or critical (got \"loud\")"
                    }
                  },
                  "invalid_enrichment_type": {
                    "value": {
                      "code": "invalid_enrichment_type",
                      "message": "enrichment.exclude_types: unknown feature_type \"made_up\""
                    }
                  },
                  "invalid_hos_ruleset": {
                    "value": {
                      "code": "invalid_hos_ruleset",
                      "message": "truck.hos.ruleset \"aetr\" is not a known regime"
                    }
                  },
                  "invalid_truck_profile": {
                    "value": {
                      "code": "invalid_truck_profile",
                      "message": "truck.profile \"straight_truck\" is not valid; accepted: box, straight, tractor"
                    }
                  },
                  "out_of_range_enrichment": {
                    "value": {
                      "code": "out_of_range",
                      "message": "enrichment.buffer_m must be in [10, 1000]"
                    }
                  },
                  "profile_not_found": {
                    "value": {
                      "code": "profile_not_found",
                      "message": "truck.profile_id \"9f1c…\" not found"
                    }
                  },
                  "truck_block_missing": {
                    "value": {
                      "code": "truck_block_required",
                      "message": "every routing request must include a truck block"
                    }
                  },
                  "unrouteable": {
                    "value": {
                      "code": "unrouteable",
                      "message": "no route found between the given coordinates"
                    }
                  }
                }
              }
            },
            "description": "Validation failure — see `code` for the specific reason"
          },
          "403": {
            "content": {
              "application/json": {
                "examples": {
                  "feature_channel_unavailable": {
                    "value": {
                      "code": "feature_channel_unavailable",
                      "message": "features[] (enrichment.include_features) is not available on the Free plan"
                    }
                  },
                  "feature_types_limit": {
                    "value": {
                      "code": "feature_types_limit",
                      "message": "the Starter plan allows at most 1 feature type(s) in enrichment.include_features (requested 2)"
                    }
                  },
                  "no_subscription": {
                    "value": {
                      "code": "no_subscription",
                      "message": "routing is not enabled on this plan"
                    }
                  }
                }
              }
            },
            "description": "Plan does not include the requested capability"
          },
          "429": {
            "content": {
              "application/json": {
                "examples": {
                  "daily_cap": {
                    "value": {
                      "code": "trial_daily_cap",
                      "message": "Free plan daily limit reached (5/day). Upgrade to remove the cap."
                    }
                  },
                  "perk_temporarily_unavailable": {
                    "value": {
                      "code": "perk_temporarily_unavailable",
                      "message": "Free-plan routing temporarily unavailable — please retry tomorrow or upgrade"
                    }
                  },
                  "quota_exhausted": {
                    "value": {
                      "code": "quota_exhausted",
                      "message": "Routing quota exhausted. Upgrade or buy a top-up pack to continue."
                    }
                  },
                  "routing_rate_limit": {
                    "value": {
                      "code": "routing_rate_limit",
                      "message": "routing rate limit reached. Retry in 12s."
                    }
                  }
                }
              }
            },
            "description": "Quota or rate limit hit.\n\n`routing_rate_limit` is the routing-only per-minute gate, separate\nfrom the data-API rate limit. Its value varies by plan and is subject\nto change — discover it from `X-Routing-RateLimit-Limit` /\n`-Remaining` / `-Reset` (returned on every routing response) and\nhonour `Retry-After`, rather than hardcoding a figure. The live\nresponse message names your plan's actual limit.\n",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying. Sent with `routing_rate_limit`.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "502": {
            "content": {
              "application/json": {
                "example": {
                  "code": "routing_unavailable",
                  "message": "routing service unavailable"
                }
              }
            },
            "description": "Upstream routing provider unavailable"
          },
          "503": {
            "content": {
              "application/json": {
                "example": {
                  "code": "routing_unavailable",
                  "message": "routing is not configured on this deployment"
                }
              }
            },
            "description": "Routing is not configured on this deployment"
          },
          "504": {
            "content": {
              "application/json": {
                "example": {
                  "code": "routing_timeout",
                  "message": "routing service timed out"
                }
              }
            },
            "description": "Upstream routing provider timed out"
          }
        },
        "summary": "Compute a truck route with hazard enrichment",
        "tags": [
          "routing"
        ]
      }
    },
    "/routing/route/saved/recent": {
      "get": {
        "description": "Returns the slim row shape (`id`, `label`, `is_persisted`, `created_at`,\n`expires_at`) — fetch the full payload via the by-id refetch\nendpoint. Used by the portal's \"Recent routes (last 30 min)\" section.\n",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "routes": [
                    {
                      "created_at": "2026-05-18T10:00:00Z",
                      "expires_at": "2026-05-18T10:30:00Z",
                      "id": "01HKZ9P5XJVQ6NB8E2RTH4FMW7",
                      "is_persisted": false,
                      "label": ""
                    }
                  ]
                }
              }
            },
            "description": "List returned"
          }
        },
        "summary": "List the customer's recent auto-saved + persisted routes",
        "tags": [
          "routing"
        ]
      }
    },
    "/routing/route/saved/{id}": {
      "delete": {
        "description": "Removes the row early. Works for both auto-saved and persisted rows.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "404": {
            "description": "Route not found (or owned by another customer)"
          }
        },
        "summary": "Delete a saved route",
        "tags": [
          "routing"
        ]
      },
      "get": {
        "description": "Returns the previously-computed route with `warnings[]` re-run against\ncurrent data. Does NOT count against routing quota, but is subject to\na separate refetch rate limit (1 / 30 s per route, 60 / minute per\ncustomer; both configurable). A 15-second refetch cache absorbs\nthe dispatcher's tweak-and-re-evaluate flow.\n\nAuto-saved routes (`is_persisted=false`) expire 30 minutes after the\noriginal routing call. After that, this endpoint returns `route_expired`.\n\nPass `route_index=N` to refetch warnings for an alternative route. The\nresponse always carries exactly one route in `routes[]`; the original\nrequest's other alternatives are still available via repeat calls\nwith different `route_index` values, but each response shows only the\none you asked for. Each `route_index` caches independently.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Zero-based index of the route to refetch. `0` is the primary\n(default). Valid range is `0 ≤ N \u003c len(routes)` against the\nsaved row's stored response. Out-of-range returns\n`invalid_route_index` (400).\n",
            "in": "query",
            "name": "route_index",
            "required": false,
            "schema": {
              "default": 0,
              "minimum": 0,
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Route returned (warnings re-evaluated). `routes[]` is a single-element array carrying the chosen `route_index`."
          },
          "400": {
            "content": {
              "application/json": {
                "example": {
                  "code": "invalid_route_index",
                  "message": "route_index 3 out of range (have 2 route(s))"
                }
              }
            },
            "description": "route_index out of range or invalid"
          },
          "404": {
            "content": {
              "application/json": {
                "examples": {
                  "expired": {
                    "value": {
                      "code": "route_expired",
                      "message": "route auto-save window has expired"
                    }
                  },
                  "not_found": {
                    "value": {
                      "code": "route_not_found",
                      "message": "route not found"
                    }
                  }
                }
              }
            },
            "description": "Route not found or expired"
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "refetch_rate_limit_exceeded",
                  "message": "refetch rate limit exceeded"
                }
              }
            },
            "description": "Refetch rate limit exceeded"
          }
        },
        "summary": "Refetch a saved route with re-evaluated warnings",
        "tags": [
          "routing"
        ]
      }
    },
    "/routing/route/saved/{id}/features": {
      "get": {
        "description": "Returns amenity/POI matches along a previously-computed route, addressed\nby its saved `route_id`. This is the lazy, on-demand companion to the\ninline `features[]` channel on `/routing/route`: it does **not** call the\nrouting engine and does **not** count against routing quota — it\nspatial-joins the requested feature types against the route's stored\ngeometry and returns them sorted by distance along the route.\n\nUse it to fetch dense or optional layers a driver pulls up when they want\nthem — DOT **traffic cameras**, **fuel stations**, parking — without\npaying that cost on every route call. `cameras` and `fuel_stations` are\navailable **only** here, not on the inline `include_features` channel\n(both are dense national layers — a camera-dense corridor can match\nthousands of points, and fuel stations are mapped at every interchange;\nkeeping them off the synchronous route path keeps that call fast). Where\na feed publishes per-station prices (EU national fuel feeds), they ride\nthrough in the feature's `properties`. The result is cached per route +\ntype set and is subject to the same refetch rate limit as\n`GET /routing/route/saved/{id}` (1 / 30 s per route, 60 / minute per\ncustomer).\n\nEach returned feature carries `distance_along_route_m` (distance from the\nroute origin to the nearest point on the route), `detour_m` (perpendicular\noff-route distance to the feature), and `side` (`left` / `right` of the\ndirection of travel; omitted for features essentially on the line).\n\nAuto-saved routes (`is_persisted=false`) expire 30 minutes after the\noriginal routing call; after that this endpoint returns `route_expired`.\n\n**Plan-gated by count**, identically to `include_features`: each plan caps\nhow many distinct feature types you may request at once (Free: none; the\ncap rises with tier; Enterprise: unlimited). Your cap is published as\n`routing_max_feature_types` on `GET /customer/plans`.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Comma-separated POI feature types to return (e.g.\n`cameras,truck_parking`). Distinct types are counted against your\nplan's `routing_max_feature_types` cap.\n",
            "explode": false,
            "in": "query",
            "name": "types",
            "required": true,
            "schema": {
              "items": {
                "enum": [
                  "cameras",
                  "fuel_stations",
                  "truck_parking",
                  "truck_rest_areas",
                  "rest_areas",
                  "service_plazas",
                  "ev_charging",
                  "certified_scales",
                  "truck_repair",
                  "truck_services",
                  "truck_washes"
                ],
                "type": "string"
              },
              "type": "array"
            },
            "style": "form"
          },
          {
            "description": "Corridor half-width in metres for the spatial match. Default 100.",
            "in": "query",
            "name": "buffer_m",
            "required": false,
            "schema": {
              "default": 100,
              "maximum": 1000,
              "minimum": 10,
              "type": "integer"
            }
          },
          {
            "description": "Only return features within this distance from the route origin. 0 (default) = unlimited.",
            "in": "query",
            "name": "max_distance_m",
            "required": false,
            "schema": {
              "default": 0,
              "minimum": 0,
              "type": "integer"
            }
          },
          {
            "description": "Zero-based index of the route to query against. `0` is the primary\n(default). Out-of-range returns `invalid_route_index` (400).\n",
            "in": "query",
            "name": "route_index",
            "required": false,
            "schema": {
              "default": 0,
              "minimum": 0,
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "count": 3,
                  "features": [
                    {
                      "detour_m": 35,
                      "distance_along_route_m": 18400,
                      "geometry": {
                        "coordinates": [
                          -76.61,
                          39.29
                        ],
                        "type": "Point"
                      },
                      "name": "I-95 @ Exit 23",
                      "properties": {
                        "image_url": "https://chart.maryland.gov/cameras/5571.jpg",
                        "roadway": "I-95 NB"
                      },
                      "side": "right",
                      "source": "MD 511",
                      "source_id": "feat_cameras_md_5571",
                      "type": "cameras"
                    },
                    {
                      "detour_m": 120,
                      "distance_along_route_m": 56200,
                      "geometry": {
                        "coordinates": [
                          2.86,
                          48.84
                        ],
                        "type": "Point"
                      },
                      "name": "TotalEnergies A4 Coutevroult",
                      "properties": {
                        "brand": "TotalEnergies",
                        "fuel_types": [
                          "diesel",
                          "octane_95"
                        ],
                        "gazole_prix": 1.799
                      },
                      "side": "left",
                      "source": "FRA",
                      "source_id": "77131004",
                      "type": "fuel_stations"
                    },
                    {
                      "as_of": "2026-05-18T10:55:00Z",
                      "available_spots": 18,
                      "capacity": 40,
                      "detour_m": 90,
                      "distance_along_route_m": 142000,
                      "geometry": {
                        "coordinates": [
                          -77.6,
                          44.1
                        ],
                        "type": "Point"
                      },
                      "name": "ONroute Trenton North",
                      "properties": {
                        "amenities": [
                          "restrooms",
                          "fuel"
                        ],
                        "spaces": 40
                      },
                      "side": "right",
                      "source": "ON 511",
                      "source_id": "feat_truck_parking_on_4821",
                      "type": "truck_parking"
                    }
                  ],
                  "route_id": "01HKZ9P5XJVQ6NB8E2RTH4FMW7"
                }
              }
            },
            "description": "Features along the route, sorted by distance along the route."
          },
          "400": {
            "content": {
              "application/json": {
                "examples": {
                  "invalid_type": {
                    "value": {
                      "code": "invalid_enrichment_type",
                      "message": "\"made_up\" is not a supported route feature type"
                    }
                  },
                  "missing_types": {
                    "value": {
                      "code": "missing_types",
                      "message": "types is required (comma-separated POI feature types, e.g. cameras,truck_parking)"
                    }
                  },
                  "route_index": {
                    "value": {
                      "code": "invalid_route_index",
                      "message": "route_index out of range for this saved route"
                    }
                  }
                }
              }
            },
            "description": "Missing/invalid types, bad knob, or route_index out of range"
          },
          "403": {
            "content": {
              "application/json": {
                "examples": {
                  "too_many": {
                    "value": {
                      "code": "feature_types_limit",
                      "message": "the Starter plan allows at most 1 feature type(s) in enrichment.include_features (requested 2)"
                    }
                  },
                  "unavailable": {
                    "value": {
                      "code": "feature_channel_unavailable",
                      "message": "features[] (enrichment.include_features) is not available on the Free plan"
                    }
                  }
                }
              }
            },
            "description": "Feature channel unavailable on the plan, or too many distinct types for the plan cap"
          },
          "404": {
            "content": {
              "application/json": {
                "examples": {
                  "expired": {
                    "value": {
                      "code": "route_expired",
                      "message": "route auto-save window has expired"
                    }
                  },
                  "not_found": {
                    "value": {
                      "code": "route_not_found",
                      "message": "route not found"
                    }
                  }
                }
              }
            },
            "description": "Route not found or expired"
          },
          "429": {
            "content": {
              "application/json": {
                "example": {
                  "code": "refetch_rate_limit_exceeded",
                  "message": "refetch rate limit exceeded"
                }
              }
            },
            "description": "Refetch rate limit exceeded"
          }
        },
        "summary": "Points-of-interest (cameras, fuel, parking) along a saved route",
        "tags": [
          "routing"
        ]
      }
    },
    "/routing/route/saved/{id}/persist": {
      "post": {
        "description": "Counts against the customer's per-plan persisted-route cap\n(`max_saved_routes`, or the per-customer override when set).\n\nSupplying a `notify` block opts the route into monitoring: the\nroute-monitor re-evaluates the route on a fixed interval and POSTs a\n`route.alert` webhook to `notify.webhook_id` whenever warnings appear\non — or clear from — the route. Alerts are filtered to `min_severity`\nand `types`; an optional `valid_from`/`valid_to` window bounds when\nmonitoring is active. The webhook body is\n`{ event: \"route.alert\", timestamp, data: { route_id, label,\nnew_warnings[], cleared_warnings[] } }`, HMAC-signed like every other\nRoad511 webhook.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "buffer_m": {
                    "default": 100,
                    "type": "integer"
                  },
                  "label": {
                    "type": "string"
                  },
                  "notify": {
                    "properties": {
                      "min_severity": {
                        "default": "warning",
                        "enum": [
                          "info",
                          "warning",
                          "critical"
                        ],
                        "type": "string"
                      },
                      "types": {
                        "items": {
                          "type": "string"
                        },
                        "type": "array"
                      },
                      "webhook_id": {
                        "type": "integer"
                      }
                    },
                    "type": "object"
                  },
                  "route_index": {
                    "default": 0,
                    "description": "Which alternative route to persist. `0` (default) keeps\nthe primary as it was computed. `N \u003e 0` re-enriches that\nalternative against current data and **replaces** the\nstored polyline + `routing_response.routes[]` with the\nchosen route only; other alternatives are dropped from\nthe row.\n",
                    "minimum": 0,
                    "type": "integer"
                  },
                  "valid_from": {
                    "format": "date-time",
                    "type": "string"
                  },
                  "valid_to": {
                    "format": "date-time",
                    "type": "string"
                  }
                },
                "type": "object"
              }
            }
          },
          "required": false
        },
        "responses": {
          "200": {
            "description": "Persisted"
          },
          "400": {
            "content": {
              "application/json": {
                "example": {
                  "code": "invalid_route_index",
                  "message": "route_index 3 out of range (have 2 route(s))"
                }
              }
            },
            "description": "Invalid request (e.g. route_index out of range)"
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "code": "saved_routes_quota_exhausted",
                  "message": "persisted-routes cap reached (5/5)"
                }
              }
            },
            "description": "Persisted-routes cap reached"
          },
          "404": {
            "description": "Route not found or already persisted"
          }
        },
        "summary": "Mark a saved route as persisted (keeps it past the 30-min TTL)",
        "tags": [
          "routing"
        ]
      }
    },
    "/stats": {
      "get": {
        "description": "Public metadata — no API key required.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "active_events": 4523,
                  "features_by_type": {
                    "bridge_clearances": 45200,
                    "cameras": 12450,
                    "ev_charging": 98500,
                    "rest_areas": 1580,
                    "road_conditions": 2100,
                    "signs": 4870,
                    "truck_restrictions": 8900,
                    "weather_stations": 3200
                  },
                  "jurisdictions": 47,
                  "total_features": 176800,
                  "uptime": "72h15m"
                }
              }
            },
            "description": "Event and feature counts"
          }
        },
        "security": [],
        "summary": "Basic stats",
        "tags": [
          "metadata"
        ]
      }
    },
    "/stats/summary": {
      "get": {
        "description": "Public metadata — no API key required. Jurisdictions breakdown,\nevents by type/severity, feature counts, last sync times.\n\nFreshness: the response is cached for 5 minutes. `events.total` — the\ncount of every event row ever stored, as opposed to `events.active` —\nis refreshed hourly rather than with the rest, because it is a\nfull-table count and moves by a fraction of a percent per hour. It is\nexact, not estimated; only its refresh interval differs.\n",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "events": {
                    "active": 4523,
                    "by_severity": {
                      "critical": 123,
                      "major": 780,
                      "minor": 1520,
                      "moderate": 2100
                    },
                    "by_type": {
                      "closure": 380,
                      "construction": 2150,
                      "incident": 1230,
                      "road_condition": 553,
                      "weather": 210
                    },
                    "total": 158420
                  },
                  "features": {
                    "by_type": {
                      "cameras": 12450,
                      "ev_charging": 98500,
                      "rest_areas": 1580,
                      "signs": 4870,
                      "weather_stations": 3200
                    },
                    "total": 176800
                  },
                  "jurisdictions": {
                    "countries": {
                      "CA": 12,
                      "US": 35
                    },
                    "list": [
                      {
                        "code": "CA",
                        "country": "US",
                        "name": "California"
                      },
                      {
                        "code": "ON",
                        "country": "CA",
                        "name": "Ontario"
                      }
                    ],
                    "total": 47
                  },
                  "last_sync": "2026-03-29T14:55:00Z",
                  "sources": {
                    "resources": 162,
                    "servers": 38
                  },
                  "uptime": "72h15m"
                }
              }
            },
            "description": "Summary stats object"
          }
        },
        "security": [],
        "summary": "Rich summary stats",
        "tags": [
          "metadata"
        ]
      }
    },
    "/status": {
      "get": {
        "description": "Aggregate health snapshot suitable for an unauthenticated status page or external uptime monitor.\nReturns the worker heartbeat freshness, total/healthy/warning/critical resource counts, and a\nper-jurisdiction breakdown with 7-day uptime. 60-second cache. No API key required.\n",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "critical": 5,
                  "generated_at": "2026-05-08T14:00:00Z",
                  "healthy": 295,
                  "jurisdictions": [
                    {
                      "code": "ON",
                      "country": "CA",
                      "name": "Ontario",
                      "open_circuits": 0,
                      "status": "operational",
                      "total_resources": 8,
                      "uptime_7day_percent": 99.8
                    },
                    {
                      "code": "WA",
                      "country": "US",
                      "name": "Washington",
                      "open_circuits": 1,
                      "status": "degraded",
                      "total_resources": 6,
                      "uptime_7day_percent": 94.5
                    }
                  ],
                  "overall": "operational",
                  "total_resources": 312,
                  "warning": 12,
                  "worker": {
                    "healthy": true,
                    "last_seen": "2026-05-08T13:59:50Z",
                    "staleness_seconds": 10
                  }
                }
              }
            },
            "description": "Aggregate status snapshot"
          }
        },
        "security": [],
        "summary": "Public status snapshot",
        "tags": [
          "metadata"
        ]
      }
    },
    "/truck/corridor": {
      "get": {
        "description": "Returns all truck-related restrictions within a buffer around the straight-line path\nbetween two points. Uses PostGIS spatial intersection (ST_Buffer + ST_Intersects).\n\nSearches across feature types: `bridge_clearances`, `bridges`, `weight_restrictions`,\n`truck_restrictions`, `truck_routes`, `freight_corridors`, `truck_parking`.\n\nResults are ordered by distance from the corridor centerline. Each feature includes\n`_distance_km` in its properties.\n\nOnly restrictions **in force now** are returned: a feature whose active window has\nended, or has not begun, is excluded. Seasonal limits (spring load restrictions)\ncarry their window on `start_time`/`end_time` and are otherwise live year-round.\n\n**This endpoint does not filter by vehicle dimensions.** `height` and `weight` are\nrejected with a 400 rather than accepted, because upstream feeds publish no\nnormalized clearance or weight for roughly 97% of the structures along a corridor.\nFiltering on what little is normalized would drop everything we cannot judge, and a\nshort list reads as \"this route is clear\" — the one wrong answer that matters.\n\nThis endpoint gives you the **inventory** along the corridor; deciding what conflicts\nwith a specific vehicle is yours to do, or use `POST /routing/route`, which follows a\nreal route and emits a warning per restriction it can actually evaluate against the\ntruck profile.\n\nRequires Pro+ plan with truck data access.\n",
        "parameters": [
          {
            "description": "Origin latitude",
            "in": "query",
            "name": "from_lat",
            "required": true,
            "schema": {
              "type": "number"
            }
          },
          {
            "description": "Origin longitude",
            "in": "query",
            "name": "from_lng",
            "required": true,
            "schema": {
              "type": "number"
            }
          },
          {
            "description": "Destination latitude",
            "in": "query",
            "name": "to_lat",
            "required": true,
            "schema": {
              "type": "number"
            }
          },
          {
            "description": "Destination longitude",
            "in": "query",
            "name": "to_lng",
            "required": true,
            "schema": {
              "type": "number"
            }
          },
          {
            "description": "Buffer distance around corridor in km (default 5, max 50)",
            "in": "query",
            "name": "buffer_km",
            "schema": {
              "default": 5,
              "maximum": 50,
              "type": "number"
            }
          },
          {
            "description": "Filter by jurisdiction code (e.g., WA, NY, ON)",
            "in": "query",
            "name": "jurisdiction",
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Maximum results (default 500, max 2000)",
            "in": "query",
            "name": "limit",
            "schema": {
              "default": 500,
              "maximum": 2000,
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "corridor": {
                    "buffer_km": 5,
                    "distance_km": 58.3,
                    "from": [
                      43.65,
                      -79.38
                    ],
                    "to": [
                      43.25,
                      -79.87
                    ]
                  },
                  "data": [
                    {
                      "feature_type": "bridge_clearances",
                      "id": "on-br-qew-001",
                      "is_active": true,
                      "jurisdiction": "ON",
                      "latitude": 43.4521,
                      "longitude": -79.7032,
                      "name": "QEW Overpass at Trafalgar Rd",
                      "properties": {
                        "_distance_km": 1.2,
                        "clearance_m": 4.35,
                        "over_road": "Trafalgar Rd",
                        "road_name": "QEW"
                      },
                      "source": "on_arcgis"
                    },
                    {
                      "feature_type": "weight_restrictions",
                      "id": "on-wr-hwy6-003",
                      "is_active": true,
                      "jurisdiction": "ON",
                      "latitude": 43.3812,
                      "longitude": -79.7654,
                      "name": "Hwy 6 Spring Load Restriction",
                      "properties": {
                        "_distance_km": 3.8,
                        "max_weight_tonnes": 5,
                        "restriction_type": "spring_load"
                      },
                      "source": "on_arcgis"
                    }
                  ],
                  "limit": 500,
                  "total": 2
                },
                "schema": {
                  "properties": {
                    "corridor": {
                      "properties": {
                        "buffer_km": {
                          "type": "number"
                        },
                        "distance_km": {
                          "type": "number"
                        },
                        "from": {
                          "items": {
                            "type": "number"
                          },
                          "type": "array"
                        },
                        "to": {
                          "items": {
                            "type": "number"
                          },
                          "type": "array"
                        }
                      },
                      "type": "object"
                    },
                    "data": {
                      "items": {
                        "$ref": "#/components/schemas/Feature"
                      },
                      "type": "array"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "total": {
                      "type": "integer"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Corridor restrictions with metadata"
          },
          "400": {
            "content": {
              "application/json": {
                "example": {
                  "error": "from_lat, from_lng, to_lat, to_lng are required"
                }
              }
            },
            "description": "Missing required coordinates"
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "error": "API key required"
                }
              }
            },
            "description": "API key required"
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "error": "truck data access requires Pro plan or higher"
                }
              }
            },
            "description": "Plan does not include truck data access"
          }
        },
        "summary": "Query truck restrictions along a corridor",
        "tags": [
          "truck"
        ]
      }
    },
    "/truck/corridor/geojson": {
      "get": {
        "description": "Same query as `/truck/corridor` but returns results as a GeoJSON FeatureCollection\nfor direct map rendering. Requires Pro+ plan with truck data + GeoJSON access.\n",
        "parameters": [
          {
            "in": "query",
            "name": "from_lat",
            "required": true,
            "schema": {
              "type": "number"
            }
          },
          {
            "in": "query",
            "name": "from_lng",
            "required": true,
            "schema": {
              "type": "number"
            }
          },
          {
            "in": "query",
            "name": "to_lat",
            "required": true,
            "schema": {
              "type": "number"
            }
          },
          {
            "in": "query",
            "name": "to_lng",
            "required": true,
            "schema": {
              "type": "number"
            }
          },
          {
            "in": "query",
            "name": "buffer_km",
            "schema": {
              "default": 5,
              "maximum": 50,
              "type": "number"
            }
          },
          {
            "in": "query",
            "name": "jurisdiction",
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "default": 500,
              "maximum": 2000,
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/geo+json": {
                "example": {
                  "features": [
                    {
                      "geometry": {
                        "coordinates": [
                          -79.7032,
                          43.4521
                        ],
                        "type": "Point"
                      },
                      "properties": {
                        "_distance_km": 1.2,
                        "clearance_m": 4.35,
                        "feature_type": "bridge_clearances",
                        "id": "on-br-qew-001",
                        "name": "QEW Overpass at Trafalgar Rd"
                      },
                      "type": "Feature"
                    },
                    {
                      "geometry": {
                        "coordinates": [
                          [
                            -79.76,
                            43.38
                          ],
                          [
                            -79.77,
                            43.39
                          ]
                        ],
                        "type": "LineString"
                      },
                      "properties": {
                        "_distance_km": 3.8,
                        "feature_type": "weight_restrictions",
                        "id": "on-wr-hwy6-003",
                        "max_weight_tonnes": 5,
                        "name": "Hwy 6 Spring Load Restriction"
                      },
                      "type": "Feature"
                    }
                  ],
                  "type": "FeatureCollection"
                },
                "schema": {
                  "properties": {
                    "features": {
                      "items": {
                        "type": "object"
                      },
                      "type": "array"
                    },
                    "type": {
                      "enum": [
                        "FeatureCollection"
                      ],
                      "type": "string"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "GeoJSON FeatureCollection"
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "error": "truck data access requires Pro plan or higher"
                }
              }
            },
            "description": "Plan does not include truck data or GeoJSON access"
          }
        },
        "summary": "Query truck restrictions along a corridor (GeoJSON)",
        "tags": [
          "truck"
        ]
      }
    }
  },
  "security": [
    {
      "ApiKeyHeader": []
    },
    {
      "ApiKeyQuery": []
    }
  ],
  "servers": [
    {
      "description": "Production",
      "url": "https://api.road511.com/api/v1"
    },
    {
      "description": "Local development",
      "url": "http://localhost:8080/api/v1"
    }
  ],
  "tags": [
    {
      "description": "Traffic events (incidents, construction, closures, etc.)",
      "name": "events"
    },
    {
      "description": "Point-of-interest features (cameras, rest areas, signs, etc.)",
      "name": "features"
    },
    {
      "description": "Map-optimized endpoints (compact mode, same data)",
      "name": "map"
    },
    {
      "description": "Historical analytics and trends (Pro+ plans)",
      "name": "analytics"
    },
    {
      "description": "Truck corridor restriction queries (Pro+ plans with truck data access)",
      "name": "truck"
    },
    {
      "description": "Truck routing — compute a hazard-enriched route, refetch / persist\n/ delete saved routes. Counts against monthly routing quota (subscription\nbucket first, then top-ups FIFO).\n",
      "name": "routing"
    },
    {
      "description": "Jurisdictions, stats, health",
      "name": "metadata"
    },
    {
      "description": "Data-source licensing and attribution",
      "name": "data-sources"
    },
    {
      "description": "Regional fuel prices — weekly US state-level (EIA), monthly Canadian province-level (StatCan)",
      "name": "fuel-prices"
    }
  ]
}