Navigation WebSocket
Navigation WebSocket
Instead of a PATCH every second or two, keep one WebSocket open for each navigation session. Positions go up it. Progress, new routes, alternative routes and hazard alerts come down it.
Create a session with POST /navigation/sessions first. Ending it stays on REST too. Keep PATCH /navigation/sessions/{id} as your fallback for when the socket is down.
Connect
wss://navvo.io/api/v1/navigation/sessions/{sessionId}/live
Authenticate during the handshake with the credentials you use for REST.
| Method | How |
|---|---|
| Signed-in user | Authorization: Bearer <JWT> header |
| API key | X-Api-Key: <key> header |
| Client that cannot set headers | ?token=<JWT> query parameter |
Use a header whenever you can, because a token in a URL can end up in logs.
Send a WebSocket ping every 25 seconds. If no pong arrives within 10 seconds, close the socket and reconnect. The server drops a connection that has been silent for 90 seconds.
A session has one socket at a time. A new connection replaces the previous one.
Message format
Every message is a UTF-8 JSON text frame.
{ "type": "<string>", "timestamp": "2026-11-10T11:05:00.123Z", "payload": { } }
Limits: 16 KB per message and 20 messages per second. Frames over the rate are dropped.
Messages you send
location_update
Send one for each valid GPS position, at 1 to 2 per second.
{
"type": "location_update",
"timestamp": "2026-11-10T11:05:01.450Z",
"payload": {
"lat": 31.953949,
"lng": 35.910635,
"bearing": 215.4,
"speed": 18.2,
"routeRevision": 2,
"capturedAt": "2026-11-10T11:05:01.400Z"
}
}
speed of at least 2 m/s.A bad bearing or speed is ignored and the position still counts. While the server is busy, only your newest position is kept.
request_reroute
Ask for a new route from where the vehicle is, for example from a Reroute button. Off-route detection does not need this, because the server works it out from your positions. You can send one every 5 seconds.
{
"type": "request_reroute",
"timestamp": "2026-11-10T11:05:03.000Z",
"payload": {
"reason": "manual",
"currentLocation": { "lat": 31.9542, "lng": 35.9101 },
"routeRevision": 2
}
}
select_alternative
Switch to a route offered by alternative_available.
{
"type": "select_alternative",
"timestamp": "2026-11-10T11:05:10.000Z",
"payload": { "alternativeId": "alt_3fa91c0b27de", "routeRevision": 2 }
}
Messages you receive
progress
One for each position the server processes.
{
"type": "progress",
"timestamp": "2026-11-10T11:05:01.490Z",
"payload": {
"routeRevision": 2,
"staleFix": false,
"offRoute": false,
"wrongWay": false,
"arrived": false,
"shapeIndex": 184,
"snapped": { "lat": 31.95394, "lng": 35.91062 },
"distanceToRouteMeters": 2,
"distanceRemainingMeters": 8450,
"durationRemainingSeconds": 580,
"distanceToNextManeuverMeters": 350,
"progressFraction": 0.15,
"nextManeuver": {
"instruction": "Keep left toward Zahran St",
"verbalAlert": "In 350 meters, keep left toward Zahran Street",
"type": "fork",
"modifier": "left",
"maneuverCode": 24
}
}
}
When staleFix is true, ignore this message's progress values. nextManeuver can be null.
type is one of depart, arrive, continue, turn, uturn, ramp, exit, fork, merge, roundabout, ferry. modifier is left, right, slight_left, slight_right, sharp_left, sharp_right, straight, enter, exit, or null.
route_updated
The route changed. Replace the route you are showing and store the new routeRevision. Ignore one with a lower revision than you already have.
{
"type": "route_updated",
"timestamp": "2026-11-10T11:05:04.120Z",
"payload": {
"routeRevision": 3,
"reason": "off_route_auto_recovery",
"totalDistanceMeters": 8920,
"totalDurationSeconds": 610,
"routeGeometry": [[31.9542, 35.9101], [31.95435, 35.91001]],
"routeShapeEncoded": "…",
"maneuvers": [],
"trafficSegments": []
}
}
reason | Meaning |
|---|---|
off_route_auto_recovery | The vehicle left the route |
wrong_way_auto_recovery | The vehicle went against the route for 3 seconds |
reroute_requested | Answer to request_reroute, or a recalculation over REST |
alternative_selected | Answer to select_alternative |
resync | Your routeRevision was behind, for example after a reconnect |
routeGeometry is [lat, lng] pairs, and routeShapeEncoded is the same line as polyline6. In maneuvers, type is the text value above and maneuverCode is the numeric code. trafficSegments is currently always empty.
alternative_available
Another way to reach the destination. The server checks about once a minute and offers routes at most 5 minutes slower.
{
"type": "alternative_available",
"timestamp": "2026-11-10T11:06:00.000Z",
"payload": {
"alternativeId": "alt_3fa91c0b27de",
"routeRevision": 3,
"name": "Via Zahran St",
"summary": "Save 4 min",
"durationDifferenceSeconds": -240,
"distanceDifferenceMeters": 350,
"forkPoint": { "lat": 31.9551, "lng": 35.9082 },
"routeGeometry": [[31.9551, 35.9082]],
"routeShapeEncoded": "…",
"expiresAt": "2026-11-10T11:08:00.000Z"
}
}
Stop showing an offer at expiresAt or when a route_updated arrives. The same alternativeId is the same alternative.
hazard_alert
Sent once per hazard, when it is within 600 m ahead on the route.
{
"type": "hazard_alert",
"timestamp": "2026-11-10T11:06:30.000Z",
"payload": {
"alertId": "inc_77492",
"category": "speed_camera",
"severity": "medium",
"distanceMeters": 600,
"location": { "lat": 31.9582, "lng": 35.9015 },
"voiceAlert": "Speed camera reported ahead in 600 meters",
"speedLimitKmh": 80
}
}
category is speed_camera or an incident type such as accident, closure, construction or jam. speedLimitKmh only appears when the limit is known.
error
A message could not be used. The socket stays open.
{ "type": "error", "timestamp": "…", "payload": { "code": "INVALID_MESSAGE", "detail": "…" } }
Codes: INVALID_JSON, INVALID_MESSAGE, UNKNOWN_TYPE, ALTERNATIVE_UNAVAILABLE, RATE_LIMITED, INTERNAL.
Route revisions
routeRevision keeps your app and the server in step, over the socket and over PATCH alike.
- Store the highest revision you have applied.
- Send it with every position.
- After any gap, send a position with your stored revision. If you are behind, you receive
route_updatedwith reasonresync.
Disconnects
| Close code | Meaning | What to do |
|---|---|---|
1000 | Trip ended | Stop |
4001 | Session missing, expired or ended | Stop. Start a new session if still navigating |
4003 | Missing or invalid credentials | Stop. Refresh the login, then reconnect once |
4009 | A newer connection took over | Stop |
1011 | Temporary server problem | Reconnect |
1012 | Service restarting | Reconnect |
1013 | Service temporarily off | Reconnect, and use PATCH meanwhile |
| Anything else | Network loss or server restart | Reconnect |
A handshake can also fail with HTTP 503 while the service is off. Treat that like 1013.
To reconnect, retry after 1 s, 2 s and 4 s, then every 4 s. Once the socket has been down for more than 5 seconds, send PATCH /navigation/sessions/{id} with routeRevision and capturedAt. When the socket returns, stop PATCH and send a location_update straight away.
End a trip
POST /api/v1/navigation/sessions/{id}/complete
{ "rating": "up", "message": "Great route", "distanceKm": 12.4, "durationSec": 900 }
Every field is optional. The trip ends and the socket closes with 1000. To cancel a trip, use DELETE /navigation/sessions/{id}.