Navvo
API Reference

Navigation WebSocket

Send GPS positions and receive progress, new routes, alternative routes and hazard alerts over one live connection.

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.

MethodHow
Signed-in userAuthorization: Bearer <JWT> header
API keyX-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"
  }
}
lat, lng
number required
The position.
bearing
number
GPS course in degrees clockwise from north. Never send a compass reading. It is only used alongside a speed of at least 2 m/s.
speed
number
Metres per second.
routeRevision
integer
The revision of the route you are following. Always send it.
capturedAt
string
ISO 8601 time the GPS position was taken. Always send it.

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": []
  }
}
reasonMeaning
off_route_auto_recoveryThe vehicle left the route
wrong_way_auto_recoveryThe vehicle went against the route for 3 seconds
reroute_requestedAnswer to request_reroute, or a recalculation over REST
alternative_selectedAnswer to select_alternative
resyncYour 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.

  1. Store the highest revision you have applied.
  2. Send it with every position.
  3. After any gap, send a position with your stored revision. If you are behind, you receive route_updated with reason resync.

Disconnects

Close codeMeaningWhat to do
1000Trip endedStop
4001Session missing, expired or endedStop. Start a new session if still navigating
4003Missing or invalid credentialsStop. Refresh the login, then reconnect once
4009A newer connection took overStop
1011Temporary server problemReconnect
1012Service restartingReconnect
1013Service temporarily offReconnect, and use PATCH meanwhile
Anything elseNetwork loss or server restartReconnect

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}.

Copyright © 2026