{
  "openapi": "3.1.0",
  "info": {
    "title": "PERCEPTA Data Distribution API",
    "version": "1.0.0-draft",
    "summary": "Traffic events detected from CCTV, plus resident reports.",
    "description": "Road-traffic events for a granted area: near misses, queues, flooding,\nroadworks, accidents, harsh manoeuvres, and reports submitted by residents.\n\n**Read the provenance fields.** Nothing here has been reviewed against\nvideo yet, speeds carry an unresolved range-dependent bias, and harsh\nmanoeuvre counts are a floor rather than a census. Every response says so\nin machine-readable fields — `provisional`, `is_lower_bound`,\n`speed_comparable_across_points` — because a caveat in prose never reaches\nwhoever writes the aggregation.\n\n**Endpoints marked *Planned* are described but not yet answering.** They\nare shown so the intended shape is reviewable; calling one returns 404\ntoday. Everything else is live.\n\n**What is not here.** Per-vehicle records are sold under individual\ncontract and are not reachable with a standard key. Video and recording\npointers are never served, at any authentication level. Resident reports\nare delivered without their free text or photographs.\n",
    "contact": {
      "name": "PERCEPTA",
      "email": "hideki@avantirnd.com"
    }
  },
  "servers": [
    {
      "url": "https://t0twj9kcx4.execute-api.us-west-2.amazonaws.com/v1",
      "description": "Current deployment (Chiang Mai)"
    }
  ],
  "security": [
    {
      "ApiKey": []
    }
  ],
  "tags": [
    {
      "name": "Events",
      "description": "Individual detected events and resident reports."
    },
    {
      "name": "Aggregates",
      "description": "Pre-computed counts and distributions. Cheaper than counting events yourself."
    },
    {
      "name": "Exports",
      "description": "Bulk delivery for wide ranges. Use these instead of paging `/events` over months."
    },
    {
      "name": "Meta",
      "description": "Vocabulary and definitions."
    },
    {
      "name": "Account",
      "description": "Your own usage and quota."
    }
  ],
  "paths": {
    "/events": {
      "get": {
        "tags": [
          "Events"
        ],
        "operationId": "listEvents",
        "summary": "Search events",
        "description": "Events in a time range, within your granted area.\n\nThe range is capped at 31 days. For anything wider, request an export\n(`POST /exports`) — paging a year out of this endpoint costs far more\non both sides than downloading the files.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/From"
          },
          {
            "$ref": "#/components/parameters/To"
          },
          {
            "name": "event_type",
            "in": "query",
            "description": "Restrict to these types. Omit for every type you may read.",
            "schema": {
              "type": "array",
              "items": {
                "$ref": "#/components/schemas/EventType"
              }
            },
            "style": "form",
            "explode": false
          },
          {
            "name": "monitoring_point_id",
            "in": "query",
            "description": "Restrict to these monitoring points.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "style": "form",
            "explode": false
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of events.",
            "headers": {
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/QuotaRemaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EventPage"
                }
              },
              "application/x-ndjson": {
                "schema": {
                  "$ref": "#/components/schemas/Event"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/events/{event_id}": {
      "get": {
        "x-status": "planned",
        "tags": [
          "Events"
        ],
        "operationId": "getEvent",
        "summary": "One event",
        "parameters": [
          {
            "name": "event_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The event.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Event"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/aggregates/by-point": {
      "get": {
        "tags": [
          "Aggregates"
        ],
        "operationId": "aggregateByPoint",
        "summary": "Counts per monitoring point",
        "description": "Ranked busiest first, with a breakdown by type.\n\n`rate_per_observed_hour` is the fairer comparison between locations:\na raw count rewards whichever camera watched longest, so \"most events\"\noften means \"busiest road\" rather than \"most dangerous\".\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/From"
          },
          {
            "$ref": "#/components/parameters/To"
          },
          {
            "name": "event_type",
            "in": "query",
            "schema": {
              "$ref": "#/components/schemas/EventType"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Counts per point.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PointAggregatePage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/aggregates/timeseries": {
      "get": {
        "tags": [
          "Aggregates"
        ],
        "operationId": "aggregateTimeseries",
        "summary": "Counts over time",
        "parameters": [
          {
            "$ref": "#/components/parameters/From"
          },
          {
            "$ref": "#/components/parameters/To"
          },
          {
            "name": "bucket",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "hour",
                "day",
                "week"
              ],
              "default": "day"
            }
          },
          {
            "name": "event_type",
            "in": "query",
            "schema": {
              "$ref": "#/components/schemas/EventType"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One row per bucket.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TimeseriesPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/aggregates/distributions": {
      "get": {
        "tags": [
          "Aggregates"
        ],
        "operationId": "aggregateDistributions",
        "summary": "Histograms",
        "description": "Speeds in 5 km/h bins, vehicles by class, queue sizes, harsh-manoeuvre\nmagnitudes.\n\nSpeed histograms carry the same caveat as individual speeds: the bias\nis range-dependent, so comparing one location with itself over time is\nsupportable and ranking locations against each other is not.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/From"
          },
          {
            "$ref": "#/components/parameters/To"
          },
          {
            "name": "monitoring_point_id",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Histograms.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Distributions"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/aggregates/density": {
      "get": {
        "tags": [
          "Aggregates"
        ],
        "operationId": "aggregateDensity",
        "summary": "Geohash density",
        "description": "For heat maps. **Density of detected events, not a rate of accidents** —\na cell is bright because things were seen there, which is a statement\nabout observation as much as about risk.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/From"
          },
          {
            "$ref": "#/components/parameters/To"
          },
          {
            "name": "precision",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 4,
              "maximum": 8,
              "default": 7
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Cells.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DensityPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/monitoring-points": {
      "get": {
        "x-status": "planned",
        "tags": [
          "Meta"
        ],
        "operationId": "listMonitoringPoints",
        "summary": "Monitoring points you may read",
        "responses": {
          "200": {
            "description": "Points.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MonitoringPointPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/meta/event-types": {
      "get": {
        "tags": [
          "Meta"
        ],
        "operationId": "listEventTypes",
        "summary": "Event types and what they mean",
        "description": "Definitions in machine-readable form. Worth reading before aggregating:\n`near_miss` is a candidate conflict rather than a confirmed one, and\n`traffic_count` is a 15-minute bucket rather than a thing that happened\nto somebody.\n",
        "security": [],
        "responses": {
          "200": {
            "description": "Definitions.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EventTypeDefinitions"
                }
              }
            }
          }
        }
      }
    },
    "/exports": {
      "get": {
        "x-status": "planned",
        "tags": [
          "Exports"
        ],
        "operationId": "listExports",
        "summary": "Your export requests",
        "responses": {
          "200": {
            "description": "Exports.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Export"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "x-status": "planned",
        "tags": [
          "Exports"
        ],
        "operationId": "createExport",
        "summary": "Request a bulk export",
        "description": "Asynchronous. Poll `GET /exports/{export_id}` for the signed URL.\n\nThis is the right way to take a wide range: the files are built once\nand served from object storage, rather than re-read out of the\ndatabase for every request.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ExportRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Export"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/exports/{export_id}": {
      "get": {
        "x-status": "planned",
        "tags": [
          "Exports"
        ],
        "operationId": "getExport",
        "summary": "Export status and download URL",
        "parameters": [
          {
            "name": "export_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Status, plus a signed URL once ready.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Export"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/usage": {
      "get": {
        "x-status": "planned",
        "tags": [
          "Account"
        ],
        "operationId": "getUsage",
        "summary": "Your usage and quota",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Daily usage.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UsagePage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "`Authorization: Bearer prc_live_<id>_<secret>`\n\nIssued once and never retrievable afterwards — we store only a hash.\nLost keys are rotated, not recovered. `prc_test_*` keys reach the\nsandbox only.\n"
      }
    },
    "parameters": {
      "From": {
        "name": "from",
        "in": "query",
        "required": true,
        "description": "Start of the window, epoch milliseconds UTC, inclusive.",
        "schema": {
          "type": "integer",
          "format": "int64"
        }
      },
      "To": {
        "name": "to",
        "in": "query",
        "required": true,
        "description": "End of the window, epoch milliseconds UTC, exclusive. At most 31 days after `from`.",
        "schema": {
          "type": "integer",
          "format": "int64"
        }
      },
      "Limit": {
        "name": "limit",
        "in": "query",
        "description": "Rows per page. Defaults to 200, capped at 1000.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 1000,
          "default": 200
        }
      },
      "Cursor": {
        "name": "cursor",
        "in": "query",
        "description": "From `next_cursor`. Opaque — do not parse or construct one.",
        "schema": {
          "type": "string"
        }
      }
    },
    "headers": {
      "RateLimitRemaining": {
        "description": "Requests left in the current second.",
        "schema": {
          "type": "integer"
        }
      },
      "QuotaRemaining": {
        "description": "Requests left in the current billing month.",
        "schema": {
          "type": "integer"
        }
      }
    },
    "schemas": {
      "CitizenReportStatus": {
        "type": "string",
        "description": "How far a council has got with a resident's report.\n\n**The values are the council's own, not a simplification.** The citizen\napp shows a shorter vocabulary — Received / In progress / Resolved /\nClosed — because a resident asking \"what happened to my report\" is not\nasking about the council's workload. That collapsing happens in the\napp; this field is the real state, because a subscriber aggregating\nresponse performance needs the distinction the app throws away.\n\n`Rejected` means the council decided not to act. It is not an error and\nnot a deletion: the report stands, with a reason recorded against it.\n",
        "enum": [
          "Reported",
          "Acknowledged",
          "In progress",
          "Resolved",
          "Rejected"
        ]
      },
      "EventType": {
        "type": "string",
        "description": "Types a standard key may read. `vehicle_passage` and `lane_count` are\nabsent by decision — per-vehicle data is contracted separately.\n",
        "enum": [
          "near_miss",
          "queue",
          "flooding",
          "roadworks",
          "accident",
          "harsh_braking",
          "harsh_acceleration",
          "harsh_cornering",
          "parked_vehicle",
          "wrong_way",
          "pedestrian_intrusion",
          "citizen_pothole",
          "citizen_flood",
          "citizen_traffic",
          "citizen_accident",
          "citizen_signal",
          "citizen_obstacle",
          "citizen_improvement",
          "citizen_other"
        ]
      },
      "Event": {
        "type": "object",
        "required": [
          "event_id",
          "event_type",
          "occurred_at_ms",
          "source",
          "attributes",
          "provisional",
          "video_reviewed"
        ],
        "properties": {
          "event_id": {
            "type": "string"
          },
          "event_type": {
            "$ref": "#/components/schemas/EventType"
          },
          "occurred_at_ms": {
            "type": "integer",
            "format": "int64",
            "description": "Epoch milliseconds UTC."
          },
          "monitoring_point_id": {
            "type": "string"
          },
          "latitude": {
            "type": "number",
            "format": "double"
          },
          "longitude": {
            "type": "number",
            "format": "double"
          },
          "source": {
            "type": "string",
            "enum": [
              "camera",
              "citizen"
            ],
            "description": "`camera` is a measurement; `citizen` is a person's judgement. Keep\nthem apart when counting — \"12 detections and 3 reports\" is honest,\n\"15 events\" quietly promotes opinion to sensor data.\n"
          },
          "severity": {
            "type": "string"
          },
          "attributes": {
            "type": "object",
            "description": "Type-specific fields. Allowlisted per type: an attribute added to\nour storage does not appear here until it is added deliberately.\n\nOn `citizen_*` events, `status` is where the council has got to\nwith the report, and it is one of a fixed set — see\n`CitizenReportStatus`. It changes after the event is first\npublished, so a subscriber holding a copy will see it go stale;\nre-read the window if the state matters to you.\n",
            "additionalProperties": true,
            "properties": {
              "status": {
                "$ref": "#/components/schemas/CitizenReportStatus"
              }
            }
          },
          "provisional": {
            "type": "boolean",
            "description": "Not confirmed by human review. True for everything today."
          },
          "video_reviewed": {
            "type": "boolean",
            "description": "Whether a person has checked this against the footage."
          },
          "is_lower_bound": {
            "type": "boolean",
            "description": "Present on harsh manoeuvres, and always `true`. The count kept only\nwhat exceeded its own measurement error threefold, so real events\nbelow that gate were discarded on purpose. Read as \"at least this\nmany\". Note that better tracking raises it without anyone driving\nworse.\n"
          },
          "significance_sigma": {
            "type": "number",
            "description": "How many times its own measurement error the manoeuvre exceeded."
          },
          "speed_kph": {
            "type": "number"
          },
          "speed_provisional": {
            "type": "boolean",
            "description": "Always `true` while calibration is outstanding."
          },
          "speed_bias_pct": {
            "type": "number",
            "description": "Unresolved range-dependent bias, as a percentage."
          },
          "speed_comparable_across_points": {
            "type": "boolean",
            "description": "Always `false`. The bias depends on range from the camera, so the\nsame vehicle reads differently near and far. Comparing a location\nwith itself over time is supportable; ranking locations against\neach other is not.\n"
          }
        }
      },
      "EventPage": {
        "type": "object",
        "example": {
          "data": [
            {
              "event_id": "cm-nm-8f21c0",
              "event_type": "near_miss",
              "occurred_at_ms": 1785973653636,
              "monitoring_point_id": "chiang-mai-location1",
              "latitude": 18.81067,
              "longitude": 98.94916,
              "source": "camera",
              "severity": "HIGH",
              "attributes": {
                "pet_seconds": 0.83,
                "pet_error_seconds": 0.12,
                "crossing_degrees": 56,
                "first_class": "car",
                "second_class": "motorcycle"
              },
              "provisional": true,
              "video_reviewed": false
            },
            {
              "event_id": "cm-hb-4a9e11",
              "event_type": "harsh_braking",
              "occurred_at_ms": 1785951846589,
              "monitoring_point_id": "chiang-mai-location1",
              "latitude": 18.81049,
              "longitude": 98.94967,
              "source": "camera",
              "severity": "MEDIUM",
              "attributes": {
                "magnitude_mps2": 3.44,
                "error_mps2": 0.5,
                "magnitude_g": 0.35,
                "class_name": "car",
                "window_count": 12
              },
              "provisional": true,
              "video_reviewed": false,
              "is_lower_bound": true,
              "significance_sigma": 6.9,
              "speed_kph": 35,
              "speed_provisional": true,
              "speed_bias_pct": 15,
              "speed_comparable_across_points": false
            },
            {
              "event_id": "cm-ip-77d301",
              "event_type": "parked_vehicle",
              "occurred_at_ms": 1785930012000,
              "monitoring_point_id": "chiang-mai-location2",
              "latitude": 18.78832,
              "longitude": 98.98531,
              "source": "camera",
              "severity": "MEDIUM",
              "attributes": {
                "cleared_at_ms": 1785930055000,
                "cleared_reason": "STREAM_ENDED"
              },
              "provisional": true,
              "video_reviewed": false
            },
            {
              "event_id": "cz-9931ab",
              "event_type": "citizen_pothole",
              "occurred_at_ms": 1785929000000,
              "monitoring_point_id": "w5q7hbp",
              "latitude": 18.7891,
              "longitude": 98.9848,
              "source": "citizen",
              "severity": "mid",
              "attributes": {
                "category": "pothole",
                "severity": "mid",
                "status": "Acknowledged",
                "has_image": true
              },
              "provisional": true,
              "video_reviewed": false
            }
          ],
          "next_cursor": "MTc4NTkyOTAwMDAwMDpjei05OTMxYWI",
          "meta": {
            "returned": 4,
            "window": {
              "from": 1785900000000,
              "to": 1786000000000
            }
          }
        },
        "required": [
          "data",
          "meta"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Event"
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Null at the end of the result set."
          },
          "meta": {
            "$ref": "#/components/schemas/PageMeta"
          }
        }
      },
      "PageMeta": {
        "type": "object",
        "properties": {
          "returned": {
            "type": "integer"
          },
          "window": {
            "type": "object",
            "properties": {
              "from": {
                "type": "integer",
                "format": "int64"
              },
              "to": {
                "type": "integer",
                "format": "int64"
              }
            }
          }
        }
      },
      "PointAggregate": {
        "type": "object",
        "properties": {
          "monitoring_point_id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "latitude": {
            "type": "number"
          },
          "longitude": {
            "type": "number"
          },
          "total": {
            "type": "integer"
          },
          "by_type": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            }
          },
          "top_type": {
            "$ref": "#/components/schemas/EventType"
          },
          "observed_hours": {
            "type": "number",
            "description": "Hours the camera was actually watching, from the ingest log."
          },
          "rate_per_observed_hour": {
            "type": "number"
          }
        }
      },
      "PointAggregatePage": {
        "type": "object",
        "example": {
          "data": [
            {
              "monitoring_point_id": "chiang-mai-location1",
              "latitude": 18.81067,
              "longitude": 98.94916,
              "total": 150,
              "by_type": {
                "near_miss": 111,
                "queue": 24,
                "harsh_braking": 13,
                "roadworks": 2
              },
              "top_type": "near_miss",
              "observed_hours": 24,
              "rate_per_observed_hour": 6.25
            },
            {
              "monitoring_point_id": "chiang-mai-location2",
              "latitude": 18.78832,
              "longitude": 98.98531,
              "total": 111,
              "by_type": {
                "near_miss": 88,
                "queue": 19,
                "accident": 4
              },
              "top_type": "near_miss",
              "observed_hours": 24,
              "rate_per_observed_hour": 4.625
            }
          ],
          "meta": {
            "returned": 2,
            "window": {
              "from": 1785900000000,
              "to": 1786000000000
            }
          }
        },
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PointAggregate"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/PageMeta"
          }
        }
      },
      "TimeseriesPage": {
        "type": "object",
        "example": {
          "data": [
            {
              "bucket_start_ms": 1785888000000,
              "count": 42,
              "by_type": {
                "near_miss": 31,
                "queue": 8,
                "harsh_braking": 3
              }
            },
            {
              "bucket_start_ms": 1785974400000,
              "count": 0,
              "by_type": {}
            }
          ],
          "meta": {
            "returned": 2,
            "bucket": "day",
            "window": {
              "from": 1785888000000,
              "to": 1786060800000
            }
          }
        },
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "bucket_start_ms": {
                  "type": "integer",
                  "format": "int64"
                },
                "count": {
                  "type": "integer"
                },
                "by_type": {
                  "type": "object",
                  "additionalProperties": {
                    "type": "integer"
                  }
                }
              }
            }
          },
          "meta": {
            "$ref": "#/components/schemas/PageMeta"
          }
        }
      },
      "Distributions": {
        "type": "object",
        "example": {
          "speed_kph": {
            "bins": [
              {
                "lower_kph": 20,
                "upper_kph": 25,
                "count": 184
              },
              {
                "lower_kph": 25,
                "upper_kph": 30,
                "count": 402
              },
              {
                "lower_kph": 30,
                "upper_kph": 35,
                "count": 377
              }
            ],
            "provisional": true,
            "bias_pct": 15,
            "comparable_across_points": false
          },
          "vehicle_class": {
            "car": 38845,
            "motorcycle": 17697,
            "truck_pickup": 14111
          },
          "harsh_magnitude_g": {
            "0.3": 11,
            "0.4": 5,
            "0.5": 1
          },
          "queue_length_meters": {
            "20": 4,
            "30": 7,
            "40": 2
          }
        },
        "properties": {
          "speed_kph": {
            "type": "object",
            "description": "5 km/h bins. Carries the same provenance as individual speeds.",
            "properties": {
              "bins": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "lower_kph": {
                      "type": "number"
                    },
                    "upper_kph": {
                      "type": "number"
                    },
                    "count": {
                      "type": "integer"
                    }
                  }
                }
              },
              "provisional": {
                "type": "boolean"
              },
              "comparable_across_points": {
                "type": "boolean"
              }
            }
          },
          "vehicle_class": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            },
            "description": "Detector classes only. There is **no bicycle class** — cyclists are\nnot counted anywhere in this data. Treat any \"vulnerable road user\"\ntotal as pedestrians and motorcyclists alone.\n"
          },
          "harsh_magnitude_g": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            }
          },
          "queue_length_meters": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            }
          }
        }
      },
      "DensityPage": {
        "type": "object",
        "example": {
          "data": [
            {
              "geohash": "w5q7hbp",
              "count": 37,
              "latitude": 18.8107,
              "longitude": 98.9492
            },
            {
              "geohash": "w5q758k",
              "count": 22,
              "latitude": 18.7883,
              "longitude": 98.9853
            }
          ],
          "note": "Density of detected events, not a rate of accidents.",
          "meta": {
            "returned": 2,
            "precision": 7,
            "window": {
              "from": 1785900000000,
              "to": 1786000000000
            }
          }
        },
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "geohash": {
                  "type": "string"
                },
                "count": {
                  "type": "integer"
                },
                "latitude": {
                  "type": "number"
                },
                "longitude": {
                  "type": "number"
                }
              }
            }
          },
          "note": {
            "type": "string",
            "description": "Always states that this is density of detections, not a rate of accidents."
          }
        }
      },
      "MonitoringPointPage": {
        "type": "object",
        "example": {
          "data": [
            {
              "monitoring_point_id": "chiang-mai-location1",
              "name": "Nimmanhaemin Rd × Soi 9",
              "latitude": 18.81067,
              "longitude": 98.94916,
              "first_observed_ms": 1782000000000
            }
          ]
        },
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "monitoring_point_id": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "latitude": {
                  "type": "number"
                },
                "longitude": {
                  "type": "number"
                },
                "first_observed_ms": {
                  "type": "integer",
                  "format": "int64"
                }
              }
            }
          }
        }
      },
      "EventTypeDefinitions": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "event_type": {
                  "$ref": "#/components/schemas/EventType"
                },
                "definition": {
                  "type": "string"
                },
                "source": {
                  "type": "string",
                  "enum": [
                    "camera",
                    "citizen"
                  ]
                },
                "caveats": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              }
            }
          }
        }
      },
      "ExportRequest": {
        "type": "object",
        "required": [
          "from",
          "to"
        ],
        "properties": {
          "from": {
            "type": "integer",
            "format": "int64"
          },
          "to": {
            "type": "integer",
            "format": "int64"
          },
          "format": {
            "type": "string",
            "enum": [
              "parquet",
              "ndjson"
            ],
            "default": "parquet"
          },
          "event_type": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EventType"
            }
          }
        }
      },
      "Export": {
        "type": "object",
        "properties": {
          "export_id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "running",
              "ready",
              "failed",
              "expired"
            ]
          },
          "format": {
            "type": "string",
            "enum": [
              "parquet",
              "ndjson"
            ]
          },
          "requested_at_ms": {
            "type": "integer",
            "format": "int64"
          },
          "ready_at_ms": {
            "type": "integer",
            "format": "int64"
          },
          "rows": {
            "type": "integer"
          },
          "bytes": {
            "type": "integer"
          },
          "download_url": {
            "type": "string",
            "format": "uri",
            "description": "Signed, valid one hour. Present only when `status` is `ready`."
          },
          "expires_at_ms": {
            "type": "integer",
            "format": "int64"
          }
        }
      },
      "UsagePage": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "date": {
                  "type": "string",
                  "format": "date"
                },
                "requests": {
                  "type": "integer"
                },
                "rows_returned": {
                  "type": "integer"
                },
                "export_bytes": {
                  "type": "integer"
                }
              }
            }
          },
          "quota": {
            "type": "object",
            "properties": {
              "requests_limit": {
                "type": "integer"
              },
              "requests_used": {
                "type": "integer"
              },
              "period_start": {
                "type": "string",
                "format": "date"
              }
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "description": "Stable and machine-readable. Branch on this, not on `message`."
              },
              "message": {
                "type": "string"
              },
              "detail": {
                "type": "object",
                "additionalProperties": true
              }
            }
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Malformed request — bad range, unknown parameter, window too wide.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "rangeTooLarge": {
                "value": {
                  "error": {
                    "code": "range_too_large",
                    "message": "The window may not exceed 31 days. Request an export instead.",
                    "detail": {
                      "max_days": 31
                    }
                  }
                }
              }
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing, malformed, revoked or expired key.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Forbidden": {
        "description": "Outside your granted area, or a tier your contract does not include.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "No such record, or not one you may read.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "TooManyRequests": {
        "description": "Rate limit or monthly quota exceeded. See `Retry-After`.",
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  }
}
