Navvo
API Reference

Traffic & Incidents

Live and typical congestion, expected (analysed) traffic for any time, traffic-aware ETAs, and crowdsourced road incidents.

Traffic & Incidents

Real-time and typical congestion derived entirely from Navvo's own anonymous probe network (there is no third-party traffic feed), expected traffic for any place and time from Navvo's analysis of past traffic in the region, plus crowdsourced road incidents.

Scope required: traffic (the traffic-aware POST /traffic/route needs both routing and traffic).

Coordinates are GeoJSON [lng, lat]. The bbox parameter is west,south,east,north. Congestion line "tiles" return GeoJSON, not binary vector tiles. Congestion vocabulary: level ∈ free | slow | heavy | standstill; congestion ∈ light | moderate | heavy | blocked.

GET /traffic/layers

Layer descriptor and legend for the live traffic layer.

{
  "layerType": "traffic",
  "name": "Live traffic",
  "source": "navvo-user-traffic",
  "tilePattern": "/api/v1/traffic/lines/tiles/{z}/{x}/{y}",
  "summary": "/api/v1/traffic/summary",
  "historicalGrid": "/api/v1/traffic/analysed/grid",
  "refreshSeconds": 60,
  "basisValues": ["current", "historical", "typical", "all-hours-fallback", "none"],
  "legend": {
    "free": { "label": "Free flow", "color": "#2dbe60" },
    "slow": { "label": "Slow", "color": "#f9a825" },
    "heavy": { "label": "Heavy", "color": "#e84b36" },
    "standstill": { "label": "Standstill", "color": "#a01a12" }
  }
}

historicalGrid is GET /traffic/analysed/grid, the usual traffic per map cell in the same legend, for roads with no live data. Only the current basis is live; every other value in basisValues is history.


GET /traffic/summary

Traffic conditions over a bbox (or the whole network if bbox is omitted): live, from Navvo's own probes, when any cover the view, and otherwise what traffic is usually like there at this hour (see below), never presented as live. Every answer has an English summary and an Arabic summaryAr, and a freshnessLabel / freshnessLabelAr pair to show beside it.

Query parameters

bbox
string
west,south,east,north. Omit to summarize all current edges (live only: without a view there is no "here" to describe).
{
  "source": "navvo-user-traffic",
  "bbox": "35.85,31.90,35.99,31.99",
  "conditions": {
    "hasData": true,
    "basis": "current",
    "sampleCount": 342,
    "sampledSegments": 128,
    "confidence": 1,
    "congestionScore": 0.42,
    "level": "slow",
    "congestion": "moderate",
    "summary": "Slow moving",
    "summaryAr": "حركة بطيئة",
    "breakdown": [
      { "level": "free", "congestion": "light", "segments": 60, "share": 46.9 },
      { "level": "slow", "congestion": "moderate", "segments": 40, "share": 31.2 }
    ]
  },
  "coverage": { "liveSegments": 128, "typicalSegments": null, "hour": 8, "hourSampleCount": null },
  "freshnessAt": "2026-06-09T08:55:00.000Z",
  "freshnessLabel": "From Navvo users",
  "freshnessLabelAr": "من مستخدمي Navvo"
}

congestionScore is a 0–1 weighted mean. sampleCount is the number of probes behind the answer and sampledSegments the road segments they cover.

No live traffic in view: the usual picture

When no live probe covers the view, the summary answers from analysed traffic at the centre of the view: what traffic is usually like there at this hour, with the day type, Ramadan, holidays, the season and the weather applied. It is labelled basis: "historical" and source: "navvo-analysed-traffic", and it is never live:

{
  "source": "navvo-analysed-traffic",
  "bbox": "28.975,41.035,28.995,41.045",
  "conditions": {
    "hasData": true,
    "basis": "historical",
    "sampleCount": 0,
    "sampledSegments": 0,
    "confidence": 1,
    "congestionScore": 0.45,
    "level": "slow",
    "congestion": "moderate",
    "summary": "Usually moderate traffic at this hour",
    "summaryAr": "ازدحام متوسط عادةً في هذه الساعة",
    "breakdown": [ … ],
    "historical": {
      "country": "TR",
      "dayType": "workday",
      "holiday": null,
      "ramadanPhase": null,
      "occasion": null,
      "expected": { "speedRatio": 0.657, "level": "moderate", "delayPct": 52 },
      "topFactors": [
        { "family": "month", "key": "10", "label": "October", "labelAr": "أكتوبر", "effectPct": -2.2 }
      ],
      "confidence": 1,
      "basis": "local"
    }
  },
  "coverage": { "liveSegments": 0, "typicalSegments": null, "hour": 15, "hourSampleCount": null },
  "freshnessAt": "2026-10-06T09:16:34.000Z",
  "freshnessLabel": "From traffic history — no live data in this view",
  "freshnessLabelAr": "من سجل حركة المرور — لا توجد بيانات حيّة في هذا العرض"
}
FieldDescription
conditions.levelOn the live palette (free, slow, heavy or standstill), so a dot or a line keeps the live colours.
conditions.summary, summaryAr"Usually … at this hour", worded from the finer analysed scale (free-flowing, light, moderate, heavy, near standstill).
conditions.hasDatatrue: the model answered. sampleCount and sampledSegments stay 0, because no probe or road segment stands behind a model answer; its evidence is in historical.
conditions.historicalexpected, as in GET /traffic/analysed; basis (the area level that answered, local … region) and confidence; dayType, holiday and ramadanPhase; occasion, a ready-to-show tag in both languages such as { "en": "Eid al-Fitr", "ar": "عيد الفطر" }, "Iftar time" or "School holidays" (null on a plain day); and topFactors, the effects a driver would notice (at least 2%), largest first.
freshnessAtWhen the model in use was fitted.

A view wider than 3° on a side, centred outside the covered region (the Middle East), or where the model has no data falls back to Navvo's per-road probe history for the current hour (source: "navvo-typical-traffic", with basis: "typical", or "all-hours-fallback" when that hour has never been observed). With nothing at all, hasData is false, level is null and freshnessLabel is "Awaiting Navvo user traffic".

Switch on conditions.basis: only current is live. historical, typical and all-hours-fallback are history: show them with their freshnessLabel (for example "From traffic history — no live data in this view"), never as "live". If you branch on source, treat navvo-analysed-traffic like navvo-typical-traffic. To colour the roads of such a view the same way, use GET /traffic/analysed/grid.

GET /traffic/lines/tiles/{z}/{x}/{y}

Live congestion lines for one tile, as a GeoJSON FeatureCollection (only edges observed in the last ~6 hours).

curl -s "https://navvo.io/api/v1/traffic/lines/tiles/14/9783/6592" -H "x-api-key: nvvo_YOUR_KEY"
{
  "type": "FeatureCollection",
  "hasData": true,
  "features": [
    {
      "type": "Feature",
      "properties": { "level": "slow", "congestion": "moderate", "color": "#f9a825", "edgeId": "edge-12345", "averageSpeedKph": 22.4, "freeFlowSpeedKph": 50 },
      "geometry": { "type": "LineString", "coordinates": [[35.9106, 31.9539], [35.9121, 31.9558]] }
    }
  ]
}

GET /traffic/lines/tile-cover

Compute the tiles covering a bbox for the live line layer, with ready-made URLs.

Query parameters

bbox
string required
west,south,east,north.
zoom
number
Tile zoom, clamped to 12–16. Defaults to 14.
maxTiles
number
Cap on returned tiles, clamped to 1–48. Defaults to 24.
{
  "layerType": "traffic", "zoom": 14, "capped": false,
  "tilePattern": "/api/v1/traffic/lines/tiles/{z}/{x}/{y}",
  "tiles": [ { "z": 14, "x": 9783, "y": 6592, "url": "/api/v1/traffic/lines/tiles/14/9783/6592" } ]
}

GET /traffic/predicted/tiles/{z}/{x}/{y}

Typical/predicted congestion for one tile (sample-weighted historical averages), as GeoJSON. Identical shape to the live tile, but each feature's properties carries "predicted": true. A companion GET /traffic/predicted/tile-cover works like the live tile-cover and additionally accepts an at ISO timestamp (folded into the returned URLs).

?at=<ISO> is now honored on the tile itself. The tile is built from the requested hour's learned per-edge profile (falling back to the all-hours typical layer when that hour has no samples), so a "traffic at 8am" preview differs from a "traffic at 6pm" preview. Omit at for the current hour.

POST /traffic/route

Compute a route and enrich its ETA with live, Navvo-sourced traffic. This is the traffic-aware sibling of POST /routing/route: it routes via the Navvo routing engine, samples congestion along the path, and returns a traffic-adjusted ETA plus the route split into colored segments.

Scopes required: routing and traffic.

Request body

locations
array required
Array of { "lon": number, "lat": number, "name"?: string }. At least 2 required.
costing
string
auto (default), truck, bicycle, pedestrian, or motor_scooter.
departureTime
string
ISO timestamp for departure; basis for arrivalTime. Defaults to now.
language
string
Narrative language. Defaults to en-US.
shape
array
Optional geometry of a route you are already displaying, as an array of [lon, lat] pairs. When provided, congestion is sampled along this exact shape and no route is re-planned — guaranteeing the colored segments match your drawn line (a re-planned route can diverge from a user-chosen alternative). Up to 10,000 points accepted (internally resampled to ≤ 2,000).
baseSeconds
number
Optional no-traffic duration (seconds) of the provided shape — used as the basis for durationInTrafficSeconds. Only meaningful together with shape; without it the traffic-adjusted ETA fields are 0 and only the colored segments are useful. A value implying an unrealistic speed is silently clamped to 15–130 km/h over the submitted shape length, so a bogus baseSeconds can't skew the delay/arrival.
{
  "locations": [ { "lon": 35.91, "lat": 31.95 }, { "lon": 35.95, "lat": 31.99 } ],
  "costing": "auto",
  "departureTime": "2026-06-09T08:30:00.000Z"
}

With your own displayed geometry (recommended when you render the route yourself):

{
  "locations": [ { "lon": 35.91, "lat": 31.95 }, { "lon": 35.95, "lat": 31.99 } ],
  "shape": [ [35.91, 31.95], [35.917, 31.958], [35.93, 31.972], [35.95, 31.99] ],
  "baseSeconds": 540,
  "costing": "auto"
}

Response (201)

{
  "provider": "navvo-user-traffic-routing",
  "available": true,
  "distanceKm": 6.42,
  "baseDurationSeconds": 540,
  "durationInTrafficSeconds": 637,
  "delaySeconds": 97,
  "departureTime": "2026-06-09T08:30:00.000Z",
  "arrivalTime": "2026-06-09T08:40:37.000Z",
  "traffic": {
    "hasData": true,
    "congestionScore": 0.33,
    "level": "slow",
    "trafficFactor": 1.18,
    "congestedShare": 50
  },
  "segments": {
    "type": "FeatureCollection",
    "features": [
      { "type": "Feature", "properties": { "level": "free", "color": "#1A73E8" }, "geometry": { "type": "LineString", "coordinates": [[35.91, 31.95], [35.92, 31.96]] } },
      { "type": "Feature", "properties": { "level": "slow", "color": "#F9A825" }, "geometry": { "type": "LineString", "coordinates": [[35.92, 31.96], [35.95, 31.99]] } }
    ]
  },
  "legend": { "free": { "label": "Free", "color": "#1A73E8" }, "slow": { "label": "Slow", "color": "#F9A825" } }
}
FieldDescription
baseDurationSecondsETA from Navvo's routing engine, no traffic.
durationInTrafficSecondsTraffic-adjusted ETA (base × trafficFactor).
delaySecondsExtra time due to traffic.
traffic.trafficFactorMultiplier applied to the base duration.
segmentsThe route as a GeoJSON FeatureCollection, colored by congestion — render directly on a map.

If the routing engine is down, available is false, durations are 0, and segments is empty.


Analysed traffic

Expected traffic for any place in the Middle East and any moment: later today, next Thursday at 17:00, the first day of Eid, a Ramadan evening or a stormy morning. It comes from Navvo's model of past traffic in the region. Every answer is labelled source: "navvo-analysed-traffic" and is never live: show it as "usually" or "expected", and use GET /traffic/summary for what is happening now. Scope required: traffic.

The model learns a typical speed for each day role and hour. Day roles are the first workday, the following workdays, the last workday and the two weekend days, so a Sunday in Amman and a Monday in Istanbul are compared like for like, whatever the local weekend. It then adjusts that speed for the month, Eid, public holidays and school holidays, the hour after iftar in Ramadan, and heavy rain, storms or dust. Those are the effects that held up when the model was fitted on earlier years and checked against a later one; the eve of a holiday, the rest of a Ramadan day, light rain, fog and snow made no reliable difference there, so they are reported in local and weather but do not change expected. Holidays, Ramadan and iftar are computed per country, so future dates need no external feed. Where a street has little history, the answer borrows from its district, its metro area, its country and finally the region; basis tells you which level answered.

speedRatio is expected speed ÷ free-flow speed (1 = an empty road). level ∈ free | light | moderate | heavy | standstill. delayPct is extra trip time versus free flow (a ratio of 0.5 gives 100). basis ∈ local | district | metro | country | region | none. none means the model has no data there yet: expected is then null, never an error. A point outside the covered region returns 400.

GET /traffic/analysed

Expected traffic at one point and moment.

Query parameters

lat
number required
Latitude.
lng
number required
Longitude.
at
string
ISO 8601 date-time. With Z or a UTC offset it is that instant. Without an offset (2026-10-09T08:00) it is local time at the point. Default: now. Up to two years ahead.
weather
string
What-if weather: clear | rain | heavy_rain | storm | fog | snow | dust. Omit it to use the forecast (next 72 hours) or, for past moments, the observed weather.
{
  "source": "navvo-analysed-traffic",
  "at": "2026-10-08T14:00:00.000Z",
  "country": "JO",
  "local": {
    "date": "2026-10-08", "time": "17:00", "weekday": "Thursday", "tz": "Asia/Amman",
    "dayType": "workday", "dayRole": "last_workday",
    "holiday": null, "eve": null, "ramadan": null,
    "schoolBreak": false, "approximate": true
  },
  "weather": { "class": "rain", "source": "forecast" },
  "expected": { "speedRatio": 0.48, "level": "heavy", "delayPct": 108 },
  "factors": [
    { "family": "weather", "key": "rain", "effectPct": -14.2, "label": "Rain", "labelAr": "مطر" },
    { "family": "month", "key": "10", "effectPct": -1.5, "label": "October", "labelAr": "أكتوبر" }
  ],
  "basis": "metro",
  "confidence": 0.58,
  "model": { "runId": 12, "trainedAt": "2026-10-06T09:12:00.000Z" }
}
FieldDescription
localLocal date, time, weekday and zone; dayType (workday, weekend, ramadan, ramadan_weekend, holiday_eve, public_holiday, eid, school_break); dayRole; the holiday or the holiday it is the eve of, in English and Arabic; during Ramadan, ramadan: { "phase": "pre_iftar", "iftar": "17:41", "minutesToIftar": 52 }; schoolBreak (estimated); and approximate, set for countries that start Hijri months by moon sighting, where dates can differ by a day.
weatherThe weather class applied and where it came from: scenario (your weather), forecast, observed or none.
expectedspeedRatio, level and delayPct; null when basis is none.
factorsWhat moved the estimate away from a normal day, largest first. effectPct is the change in speed (-12 = 12% slower). label and labelAr are ready to display.
basis, confidenceThe level of the area hierarchy the typical pattern came from, and a 0–1 confidence from the amount of history behind it.
modelThe model fit in use (runId, trainedAt), or null before the first fit.

GET /traffic/analysed/week

Seven local days × 24 hours at a point, with each day's best and worst two-hour windows between 06:00 and 23:00. This is the data behind "when should I leave?".

Query parameters

lat
number required
Latitude.
lng
number required
Longitude.
from
string
First local date, YYYY-MM-DD. Default: today at the point. Within a year of today.
{
  "source": "navvo-analysed-traffic",
  "country": "JO", "tz": "Asia/Amman", "from": "2026-10-06",
  "basis": "metro", "confidence": 0.55,
  "weather": { "source": "forecast", "note": "Forecast weather is applied to the next 72 hours only." },
  "model": { "runId": 12, "trainedAt": "2026-10-06T09:12:00.000Z" },
  "days": [
    {
      "date": "2026-10-06", "weekday": "Tuesday", "dayType": "workday", "dayRole": "workday_3",
      "holiday": null, "eve": null, "ramadan": null,
      "best":  { "from": "10:00", "to": "12:00", "startHour": 10, "speedRatio": 0.81, "level": "light", "delayPct": 23 },
      "worst": { "from": "16:00", "to": "18:00", "startHour": 16, "speedRatio": 0.47, "level": "heavy", "delayPct": 113 },
      "hours": [
        { "hour": 0, "speedRatio": 0.93, "level": "free", "delayPct": 8, "weather": "clear" }
      ]
    }
  ]
}

hours always has 24 entries (one shown here); an hour without data has null values. Forecast weather is applied to the next 72 hours only, and later hours assume normal weather. A window needs data for both of its hours. On a flat day worst is null. During Ramadan each day carries ramadan: { "iftar": "17:41" }.


GET /traffic/analysed/grid

Expected traffic for every geohash cell of a map view at one moment, for drawing the roads that have no live traffic. Each cell carries the model's level and a liveLevel: the same expected speed on the live layer's own thresholds, so the lines can use the live legend from GET /traffic/layers. Never live.

Query parameters

bbox
string required
west,south,east,north, at most 3° on a side. A larger view is a 400: zoom in.
at
string
ISO 8601 date-time, as for GET /traffic/analysed; without an offset it is local time in the view. Default: now. Rounded down to 5 minutes.
precision
number
Cell size: 6 for street cells (about 1.0 × 0.6 km in the region) or 5 for district cells (about 4.2 × 4.9 km). Default: 6 for views up to about 15 × 15 km, 5 above.
curl -s "https://navvo.io/api/v1/traffic/analysed/grid?bbox=28.975,41.035,28.995,41.045" -H "x-api-key: nvvo_YOUR_KEY"
{
  "source": "navvo-analysed-traffic",
  "at": "2026-10-06T12:45:00.000Z",
  "precision": 6,
  "extent": [28.970947, 41.033936, 29.003906, 41.050415],
  "cellSize": { "lngDeg": 0.010986328125, "latDeg": 0.0054931640625 },
  "truncated": false,
  "country": "TR",
  "countries": ["TR"],
  "local": { "date": "2026-10-06", "time": "15:45", "dayType": "workday", "holiday": null, "ramadan": null },
  "weather": { "class": "clear", "source": "forecast" },
  "cells": [
    { "g": "sxk97q", "ratio": 0.541, "level": "moderate", "liveLevel": "slow", "basis": "local" },
    { "g": "sxk97w", "ratio": 0.637, "level": "moderate", "liveLevel": "slow", "basis": "local" },
    { "g": "sxk97y", "ratio": 0.435, "level": "heavy", "liveLevel": "heavy", "basis": "local" },
    { "g": "sxk97r", "ratio": 0.591, "level": "moderate", "liveLevel": "slow", "basis": "local" },
    { "g": "sxk97x", "ratio": 0.657, "level": "moderate", "liveLevel": "slow", "basis": "local" },
    { "g": "sxk97z", "ratio": 0.503, "level": "moderate", "liveLevel": "slow", "basis": "local" },
    { "g": "sxk9e2", "ratio": 0.72, "level": "light", "liveLevel": "slow", "basis": "local" },
    { "g": "sxk9e8", "ratio": 0.549, "level": "moderate", "liveLevel": "slow", "basis": "local" },
    { "g": "sxk9eb", "ratio": 0.519, "level": "moderate", "liveLevel": "slow", "basis": "local" }
  ],
  "basis": "local"
}
FieldDescription
cellsOne entry per cell the model has an answer for: g (the cell's geohash), ratio (expected speed ÷ free-flow speed), level (free, light, moderate, heavy or standstill), liveLevel (free, slow, heavy or standstill, on the live layer's thresholds) and basis (local … region). Cells outside the covered region, or without any data, are left out.
precision, cellSize, extentThe geohash precision, one cell's size in degrees, and the [west, south, east, north] the cells cover: the view snapped to whole cells.
country, countries, localThe country most of the cells are in (each cell is read with its own country's clock and calendar), every country in the view, and the local date, time, dayType, holiday and Ramadan phase for the main one.
weatherThe weather applied, from the forecast at the centre of the view; source is "none" when none was available in time.
basisThe finest area level among the cells, or none. When it is country or region, every cell carries the same broad estimate: draw it faintly, or label it as a general pattern.
truncatedtrue when the view has more than 2,500 cells at this precision. A window centred on the view is answered; extent says which.

To draw it, take each road segment's midpoint, compute its geohash at precision, and colour the segment with that cell's liveLevel. A view entirely outside the covered region returns cells: [] with a note, not an error. Answers are cached per view and 5-minute slot, so refetching after every pan is cheap.


GET /traffic/analysed/outlook

A one- or two-sentence traffic outlook for a country's main city on a local date, in English or Arabic.

Query parameters

country
string
ISO 3166-1 alpha-2 code: JO, SA, AE, QA, KW, BH, OM, EG, LB, PS, IQ, SY, YE, TR or IR. Default JO.
date
string
Local date, YYYY-MM-DD. Default: today in that country.
lang
string
en (default) or ar.
{
  "source": "navvo-analysed-traffic",
  "country": "JO", "scope": "cc:JO", "date": "2026-10-08", "lang": "en",
  "text": "Amman, Thursday 8 October: a regular workday. Expect the busiest roads around 08:00 (moderate traffic) and 16:00 (heavy traffic), close to a usual Thursday.",
  "facts": {
    "city": "Amman", "date": "2026-10-08", "weekday": "Thursday", "dayType": "workday",
    "holiday": null, "eve": null, "iftar": null,
    "busiest": [
      { "time": "08:00", "hour": 8, "level": "moderate", "delayPct": 54 },
      { "time": "16:00", "hour": 16, "level": "heavy", "delayPct": 121 }
    ],
    "vsUsualPct": 0, "basis": "metro", "confidence": 0.55
  },
  "writer": "template",
  "stored": true,
  "updatedAt": "2026-10-06T23:41:07.000Z"
}

Outlooks for Amman, Riyadh, Dubai, Doha, Kuwait City, Manama, Muscat, Cairo and Istanbul are written every night for the next seven days (stored: true). writer is navvo-ai when Navvo's on-server model wrote the sentence, and template when the fixed template did. A model-written sentence is published only if every number in it appears in facts. vsUsualPct compares average trip time over 06:00–23:00 with a usual day of the same day role (positive = slower). Other countries and dates are built on request from the same facts (stored: false). When the model has no data for the country yet, text and facts are null and basis is "none".


GET /traffic/analysed/model

The model fits behind these answers: the five latest runs, the fit in use, and the size of the pattern tables.

{
  "source": "navvo-analysed-traffic",
  "model": { "runId": 12, "trainedAt": "2026-10-06T09:12:00.000Z" },
  "rows": { "patternBase": 184212, "patternFactors": 96540 },
  "runs": [
    { "id": 12, "kind": "grid", "status": "done", "stats": { "rows": 21800000, "baseRows": 184212, "factorRows": 96540 }, "startedAt": "2026-10-06T08:40:00.000Z", "finishedAt": "2026-10-06T09:12:00.000Z" },
    { "id": 11, "kind": "grid-holdout", "status": "done", "stats": { "validation": { "heldOutFrom": "2024-07-01", "maeSpeedRatio": { "model": 0.061, "hourOfDayOnly": 0.083 } } }, "startedAt": "2026-10-06T07:55:00.000Z", "finishedAt": "2026-10-06T08:31:00.000Z" }
  ]
}

A *-holdout run scores the model on months it was not trained on, without replacing the model in use. validation.maeSpeedRatio is the mean absolute error in speedRatio for the full model and for a simpler hour-of-day baseline. The figures above only illustrate the shape; read the live values from the endpoint.


Incidents

Crowdsourced road incidents — jams, closures, accidents, hazards, and more. Scope required: traffic.

Incident type ∈ jam, closure, accident, hazard, weather, breakdown, speed_check, construction, lane_closure, visibility. severity is 1–5. Only active, non-expired incidents are returned.

GET /incidents/types

The catalog of supported incident types — for building a "report incident" type picker. Each entry carries a human label, a default severity, the default lifetime (defaultTtlMinutes), and whether the type blocks routing (route-blocking types are routed around; soft types only alert the driver).

curl -s "https://navvo.io/api/v1/incidents/types" -H "x-api-key: $NAVVO_API_KEY"
{
  "provider": "navvo-incidents", "count": 10, "confirmBumpMinutes": 25, "maxTtlMinutes": 720,
  "types": [
    { "type": "jam", "label": "Traffic jam", "defaultSeverity": 2, "defaultTtlMinutes": 90, "routeBlocking": false },
    { "type": "closure", "label": "Road closure", "defaultSeverity": 4, "defaultTtlMinutes": 240, "routeBlocking": true },
    { "type": "accident", "label": "Accident", "defaultSeverity": 4, "defaultTtlMinutes": 120, "routeBlocking": true }
  ]
}

The type values are exactly what POST /incidents and POST /incidents/report accept; confirmBumpMinutes/maxTtlMinutes describe how confirmations extend an incident's life.

GET /incidents

List active incidents, severity-then-recency ordered, with layer metadata. items[] are the same whitelisted projection as POST /incidents (no reporter id; metadata is { subType?, photoUrl?, source }).

A malformed bbox (not west,south,east,north with west < east, south < north) returns 400 "bbox must be west,south,east,north" — even when zero incidents are active. An empty bbox= is treated as absent.

Query parameters

bbox
string
west,south,east,north spatial filter.
type
string
Filter by incident type.
limit
number
Max items, 1–250 (also capped by zoom).
zoom
string
Controls the default limit cap.
{
  "total": 1,
  "items": [
    {
      "id": "b1f2c3d4-…",
      "type": "accident",
      "status": "active",
      "location": "POINT(35.9106 31.9539)",
      "title": "Two-car collision",
      "severity": 4,
      "expiresAt": "2026-06-09T10:40:00.000Z",
      "confirmations": 3,
      "rejections": 0
    }
  ],
  "metadata": { "layerType": "incidents", "source": "navvo-incidents", "dataUrl": "/api/v1/incidents/geojson", "hasData": true }
}

items[].location is a WKT string; for parsed coordinates use the GeoJSON endpoint below.

GET /incidents/geojson

Active incidents as a GeoJSON FeatureCollection (coordinates [lng, lat]). Accepts the same bbox/type/limit/zoom filters (with the same up-front bbox validation as the list). Each feature's properties now include photoUrl (string | null) — photo evidence attached at report time. A GET /incidents/geojson/tiles/{z}/{x}/{y} per-tile variant and a GET /incidents/geojson/tile-cover planner are also available.

{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "geometry": { "type": "Point", "coordinates": [35.9106, 31.9539] },
      "properties": { "id": "b1f2c3d4-…", "type": "accident", "title": "Two-car collision", "severity": 4, "confirmations": 3, "rejections": 0, "photoUrl": null }
    }
  ],
  "metadata": { "layerType": "incidents", "hasData": true }
}

POST /incidents

Report a new incident.

Request body

type
string required
One of the incident types listed above.
lat
number required
Latitude.
lng
number required
Longitude.
title
string
Short title.
description
string
Free text.
severity
number
1–5. Defaults to 2.
ttlMinutes
number
Lifetime in minutes, 5–1440. Defaults to 180.
photoUrl
string
Optional https:// photo evidence URL (max 500 chars). A non-https or over-long value returns 400 "photoUrl must be an https URL". Stored on the incident and surfaced as a photoUrl property on the GeoJSON feature.
Optional attribution. Send your signed-in user's Authorization: Bearer <token> and the report is credited to that verified user. The legacy body reportedByUserId is honored only for anonymous callers and is deprecated; a reporter id is never returned in the response.
{ "type": "hazard", "lat": 31.9539, "lng": 35.9106, "title": "Debris on road", "severity": 3, "ttlMinutes": 120, "photoUrl": "https://cdn.example.com/hazard.jpg" }

Returns 201 with the created incident (status: "active", expiresAt set from ttlMinutes). The response is a whitelisted projection: { id, createdAt, updatedAt, type, status, location, title, description, severity, expiresAt, confirmations, rejections, metadata }, where metadata contains only subType?, photoUrl?, and source. title/description are null (not absent) when unset.

POST /incidents/report

The community-report helper — like POST /incidents but with auto-deduplication. If an active incident of the same type already exists within ~80 m, this is treated as a confirmation (bumps that incident's confirmations and refreshes its expiry) instead of creating a duplicate pin — the right behaviour when many drivers report the same jam or accident. Otherwise a new incident is created.

Same body as POST /incidents (including the optional photoUrl and Bearer attribution), plus an optional dedupMeters (number, 10–500, default 80). The body is fully validated: a bad type, non-numeric lat/lng, out-of-range severity/ttlMinutes, or an out-of-range dedupMeters now returns 400 instead of persisting garbage. The incident in both the merged:true and merged:false responses is the same whitelisted projection as POST /incidents (no reporter id leak from a merged incident).

curl -s -X POST "https://navvo.io/api/v1/incidents/report" \
  -H "x-api-key: $NAVVO_API_KEY" -H "content-type: application/json" \
  -d '{ "type": "jam", "lat": 31.97, "lng": 35.92, "severity": 3, "title": "Heavy traffic" }'
{ "provider": "navvo-incidents", "merged": true, "distanceM": 31, "confirmations": 2, "incident": { } }
FieldTypeNotes
mergedbooleantrue if folded into a nearby same-type incident (a confirmation); false if a new one was created
distanceMnumber(when merged) distance to the matched incident
confirmationsnumber(when merged) the incident's confirmation count after this report
incidentobjectthe resulting incident

Use this from a "Report" button so repeated reports of one event converge on a single, increasingly-confirmed pin rather than cluttering the map.

POST /incidents/{id}/votes

Confirm or reject an incident (crowd validation). Returns 201 with the updated incident (the whitelisted projection — no reporter id). An incident with 5+ rejections (and rejections > 2× confirmations) is auto-marked rejected and drops out of listings. An unknown incident id returns 404 "Incident not found".

Votes are de-duplicated. With a valid Bearer token, a verified user may vote once per incident — a repeat returns the incident unchanged (no counter mutation). Anonymous voters are de-duplicated per (IP, incident) for ~6 hours. Send your user's Authorization header so their vote is attributed and counted durably.

Request body

vote
string required
confirm or reject.
userId
string
Optional voter id (ignored for dedup when a Bearer token is present — the verified subject is used).
{ "vote": "confirm" }

Road edits & closures

Road edits are authored on the Navvo platform (operations dashboard) and are the authoritative, longer-lived counterpart to crowd incidents: road closures, access restrictions, custom (drawn) roads, and attribute adjustments. Active edits appear on the map, here in the API, and — for closures/restrictions — are routed around automatically (see roadEditConstraints in the route response). Each edit can be permanent or temporary (a start/end window).

Required scope: tiles

GET /road-edits/geojson

Active road edits in a viewport as a GeoJSON FeatureCollection with mixed geometry (LineString for road segments, Polygon for area closures, Point for point edits). Only edits effective now are returned (a scheduled edit appears once its start time passes; an expired or disabled one drops out).

bbox
string
Viewport bounds west,south,east,north. Omit for everything (capped).
curl -s "https://navvo.io/api/v1/road-edits/geojson?bbox=35.8,31.9,36.0,32.0" \
  -H "x-api-key: $NAVVO_API_KEY"
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "geometry": { "type": "LineString", "coordinates": [[35.911, 31.954], [35.912, 31.955]] },
      "properties": {
        "id": "…", "layerType": "road_edits", "editType": "closure", "geometryType": "line",
        "name": "Rainbow St closed", "reason": "construction", "status": "active",
        "severity": 4, "blocksRouting": true,
        "startsAt": null, "endsAt": "2026-07-01T18:00:00.000Z", "permanent": false,
        "color": "#DC2626"
      }
    }
  ],
  "metadata": { "layerType": "road_edits", "name": "Road edits", "source": "navvo-road-edits", "hasData": true }
}
PropertyNotes
editTypeclosure · restriction · custom_road · attribute
geometryTypeline · polygon · point
blocksRoutingwhen true, routing detours around this edit
permanent / startsAt / endsAtvalidity window (permanent: true ⇒ no end)
colorsuggested render colour for the edit type
custom_road edits are map annotations — they are drawn on the map but are not routable (the routing engine drives a prebuilt graph; a new drivable road needs a graph rebuild). Closures, restrictions, and adjustments take effect immediately.
Copyright © 2026