Navigation
Navigation
A server-aware navigation control plane for building a Google-Maps-class turn-by-turn experience without re-inventing the hard geometry on the client. It sits on top of Routing and Traffic: you start a session for a trip, feed it live GPS fixes, and the server computes snap-to-route progress, off-route detection, automatic re-routing, a traffic-aware ETA, arrival detection, and a rolling event log (with optional signed webhooks).
Every stateless helper (off-route, progress, arrival-check) is also available on its own if you prefer to drive the loop client-side.
Scope required: routing
x-api-key). Anonymous requests return 401. Sessions are owned by whoever created them; another caller reading or updating your session gets 403.{ lat, lng } on every Navigation body (unlike the low-level routing passthrough, which uses lon). Route geometry (routeShape) is an encoded polyline, precision 6 (a.k.a. polyline6) — decode with a factor of 1e6. All POSTs return 201 on success.POST /navigation/sessions
Start a managed navigation session. The server plans the route (via the Navvo routing engine), parses the maneuvers with cumulative distances, and returns a session you can drive with PATCH/positions.
Request body
{ "lat": number, "lng": number }.{ "lat": number, "lng": number, "name"?: string, "placeSlug"?: string }. placeSlug lets arrival detection resolve a mapped entrance.auto (default), pedestrian, bicycle, motor_scooter, truck, wheelchair.en-US, ar). Default en-US.fastest, shortest, or balanced.tolls, highways, ferries, unpaved.curl -s -X POST "https://navvo.io/api/v1/navigation/sessions" \
-H "Authorization: Bearer $NAVVO_JWT" -H "Content-Type: application/json" \
-d '{
"origin": { "lat": 31.9539, "lng": 35.9106 },
"destination": { "lat": 31.9730, "lng": 35.9200, "name": "Sports City" },
"costing": "auto", "language": "en-US"
}'
Response (201)
{
"sessionId": "b23f9bec-516c-4038-a2fa-c3a275ec598b",
"status": "active",
"costing": "auto",
"language": "en-US",
"destination": { "lat": 31.9730, "lng": 35.9200, "name": "Sports City" },
"routeShape": "…encoded polyline6…",
"maneuvers": [
{
"index": 0, "type": 1, "instruction": "Drive north.",
"verbalAlert": "Drive north.", "verbalPre": "Drive north for 300 meters.",
"verbalPost": "Continue for 300 meters.", "streetNames": ["King Abdullah St"],
"roundaboutExit": null, "exitSign": null,
"distanceMeters": 300, "timeSeconds": 34,
"lat": 31.9539, "lng": 35.9106, "cumulativeMeters": 0
}
],
"distanceMeters": 6366,
"baseSeconds": 402,
"etaSeconds": 402,
"distanceRemainingMeters": 6366,
"durationRemainingSeconds": 402,
"maneuverIndex": 0,
"eventSeq": 0,
"expiresAt": "2026-07-20T02:00:00.000Z",
"route": { "shape": "…", "maneuvers": [ … ], "distanceMeters": 6366, "etaSeconds": 402 }
}
| Field | Meaning |
|---|---|
maneuvers[] | Parsed turn list. type is a maneuver code — resolve it with GET /routing/maneuver-types. cumulativeMeters is the distance from the route start to that maneuver (used for progress). verbal* are ready for text-to-speech. |
distanceMeters / baseSeconds | Total route length and no-traffic duration. |
eventSeq | Monotonic event counter — poll GET …/events?since= with the last value you saw. |
expiresAt | Sessions auto-expire ~6 h after last activity. |
Errors: 400 if no route can be planned between the points (or costing: "truck" where truck routing is not provisioned); 401 unauthenticated.
GET /navigation/sessions/{id}
Fetch the current session snapshot (route, maneuvers, progress, status). 404 if unknown, 403 if it belongs to another caller.
PATCH /navigation/sessions/{id}
Feed the latest GPS fix. The server snaps it to the route, advances the maneuver pointer, detects arrival and off-route (auto-re-planning when off-route), appends any events, and returns the fresh progress.
Request body
Response (201)
{
"sessionId": "…", "status": "active", "maneuverIndex": 2, "eventSeq": 4,
"etaSeconds": 237, "distanceRemainingMeters": 3756, "durationRemainingSeconds": 237,
"progress": {
"offRoute": false,
"arrived": false,
"shapeIndex": 128,
"maneuverIndex": 2,
"snapped": { "lat": 31.9601, "lng": 35.9149 },
"distanceToRouteMeters": 6,
"distanceRemainingMeters": 3756,
"durationRemainingSeconds": 237,
"distanceToNextManeuverMeters": 210,
"nextManeuver": { "index": 2, "type": 15, "instruction": "Turn left onto …", "verbalAlert": "Turn left onto …", "cumulativeMeters": 3966, "lat": 31.96, "lng": 35.915 },
"progressFraction": 0.41
},
"reroute": null
}
When the fix is off the route corridor, progress.offRoute is true, an off_route event is emitted, and the server re-plans from the current position: reroute then carries the new { shape, maneuvers, distanceMeters, etaSeconds } and a rerouted event is logged. When within ~35 m of the destination the session flips to status: "arrived" and an arrived event fires.
DELETE /navigation/sessions/{id}
End the session (status: "cancelled"). Returns { "ended": true, "sessionId": "…", "status": "cancelled" }.
POST /navigation/sessions/{id}/positions
Submit a batch of recent fixes (up to 50) — useful for a background/duty-cycled uplink. The latest fix drives the authoritative progress update; the response mirrors PATCH plus accepted (count) and newEvents (events produced by this batch).
{ "positions": [ { "lat": 31.958, "lng": 35.897, "speed": 11 }, { "lat": 31.960, "lng": 35.915, "speed": 9 } ] }
GET /navigation/sessions/{id}/events
Poll the session's event log. Pass ?since=<eventSeq> to get only newer events.
{
"sessionId": "…", "status": "active", "eventSeq": 6,
"events": [
{ "seq": 4, "type": "maneuver_completed", "at": "2026-07-19T18:02:11.000Z", "data": { "maneuverIndex": 1, "instruction": "…" } },
{ "seq": 5, "type": "off_route", "at": "…", "data": { "distanceToRouteMeters": 67 } },
{ "seq": 6, "type": "rerouted", "at": "…", "data": { "reroutes": 1, "distanceMeters": 4175 } }
]
}
Event types: maneuver_approaching, maneuver_completed, off_route, rerouted, arrived. The last ~50 events are retained per session.
GET /navigation/sessions/{id}/eta
Recompute the traffic-aware ETA for the remaining route (from the last snapped position onward). This is the cheap, session-scoped alternative to polling POST /traffic/route with a hand-built shape.
{
"sessionId": "…", "available": true, "etaSeconds": 660,
"arrivalTime": "2026-07-19T18:15:00.000Z",
"delaySeconds": 120, "trafficLevel": "slow", "source": "traffic"
}
When live traffic is unavailable it returns { "available": false, "etaSeconds": <base>, "source": "base" }.
POST /navigation/off-route
Stateless. Test a single position against a route corridor — no session needed.
Request body
{ "lat": number, "lng": number }.40).Response (201)
{
"provider": "navvo-navigation",
"offRoute": true,
"distanceToRouteMeters": 67.2,
"confidence": 0.91,
"snapped": { "lat": 31.9598, "lng": 35.9141 },
"shapeIndex": 128,
"alongMeters": 2606
}
snapped is the nearest point on the route; alongMeters is how far along the route that snapped point is. 400 if routeShape does not decode to at least two points.
POST /navigation/recalculate
Re-plan the remaining route from the current location. Preserves the trip's preferences. If sessionId is provided the session's route is replaced in place (and reroutes incremented); otherwise it is a stateless one-off (supply destination).
Request body
destination/costing/preference default to the session's.{ "lat": number, "lng": number } — the new origin.sessionId is omitted.fastest / shortest / balanced.Response (201)
{
"provider": "navvo-navigation",
"sessionId": "…or null",
"costing": "auto", "preference": "balanced",
"route": { "shape": "…encoded polyline6…", "maneuvers": [ … ], "distanceMeters": 4175, "etaSeconds": 260 }
}
POST /navigation/progress
Stateless. Project a position onto a known route and get remaining distance/time, the next-maneuver index, and arrival — the same math the session PATCH runs, without a session.
Request body
{ "lat": number, "lng": number }.[{ "cumulativeMeters": number }, …] (from a session or /routing/plan) so the response can advance maneuverIndex.durationRemainingSeconds.30).Response (201)
{
"provider": "navvo-navigation",
"shapeIndex": 128, "maneuverIndex": 3,
"snapped": { "lat": 31.9598, "lng": 35.9141 },
"distanceToRouteMeters": 4.2,
"distanceRemainingMeters": 2100,
"durationRemainingSeconds": 280,
"distanceToNextManeuverMeters": 120,
"progressFraction": 0.64,
"arrived": false
}
POST /navigation/arrival-check
Stateless. Confirm arrival at a destination (and resolve the nearest mapped entrance when a placeSlug is supplied).
Request body
{ "lat": number, "lng": number, "placeSlug"?: string }.{ "lat": number, "lng": number }.destination.placeSlug for the entrance lookup.30).Response (201)
{ "provider": "navvo-navigation", "arrived": true, "distanceMeters": 12.4, "radiusMeters": 30, "entranceId": "ent_3" }
entranceId is null when the place has no mapped entrances (or none is supplied).
POST /navigation/speak
Turn a maneuver instruction into speech-ready output. Always returns the normalized text, a ready-to-use ssml string (with the correct language + a calm speaking rate), and the resolved language. When a server-side text-to-speech provider is configured it also returns an audioUrl; otherwise available is false and clients synthesize on-device from text/ssml (the default, recommended path — consistent Arabic/English pronunciation with zero extra latency).
Request body
en-US or ar-SA. Default en-US.Response (201)
{
"provider": "navvo-navigation",
"available": false,
"reason": "on_device",
"language": "en-US",
"text": "In 200 meters, turn right onto King Abdullah Street.",
"ssml": "<speak xml:lang=\"en-US\"><prosody rate=\"0.94\">In 200 meters, turn right onto King Abdullah Street.</prosody></speak>",
"audioUrl": null
}
Navigation event webhooks
If a session was created with a webhookUrl, each batch of new events is POSTed to it as { "sessionId": "…", "events": [ … ] }. Deliveries carry:
X-Navvo-Signature: sha256=<hex>— HMAC-SHA256 of the raw request body. Verify it with your webhook secret before trusting a delivery.X-Navvo-Delivery— a short idempotency token.
The URL must be https and must resolve to a public address — deliveries to loopback/private ranges are refused (SSRF protection). Delivery is best-effort (5 s timeout, no retries); poll GET …/events as the source of truth.
Related
- Maneuver code table:
GET /routing/maneuver-types. - Embed lanes + speed limits directly in a plan:
POST /routing/planwithincludeLanes/includeAttributes. - Raw traffic-aware ETA for an arbitrary shape:
POST /traffic/route. - Contribute anonymous GPS to improve live traffic (after/not during guidance):
POST /telemetry/trips.