Itinerary, Geofencing & Static Maps
Itinerary, Geofencing & Static Maps
Three building blocks for higher-order location features. Itinerary powers Atlas-style "Ask Maps" trip planning — hand Navvo a list of stops and get back an optimized visiting order with leg-by-leg distances and times, plus authenticated endpoints to save, share, and reload a user's trips. Geofencing lets B2B teams define circular or polygon zones, evaluate a device position against them, and fire webhooks the moment a device enters or leaves a zone. Static Maps renders a ready-to-embed map image you can drop straight into an <img> — perfect for emails, link previews, and server-rendered cards.
Everything is served and branded by Navvo from a single API. There are no outside services to wire up.
Scope required: routing for itinerary planning; geo for geofences; tiles for static maps. Saved-itinerary endpoints use a user session token (see below).
lat, lng). Distances are in kilometres (distanceKm) or metres (radiusM) as labelled, and durations are in minutes. Polygon rings are arrays of [lng, lat] pairs. Endpoints degrade gracefully — a planning request that can't be routed returns available:false rather than an error.POST /itinerary/plan
Plan a multi-stop trip. Give Navvo an ordered (or unordered) list of stops and it returns an optimized visiting order with the route stitched between them — leg distances, leg times, and trip totals including dwell time at each stop. This is the engine behind Atlas "Ask Maps" trip suggestions.
When you pass 3 or more stops, Navvo optimizes the visiting order to minimize total travel while keeping the first and last stops fixed (your start and end anchors); the intermediate stops are reordered. With 2 stops it simply routes between them. Add a dwellMin to any stop to budget time spent there.
curl -s "https://navvo.io/api/v1/itinerary/plan" \
-H "x-api-key: nvvo_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"stops": [
{ "name": "Amman Citadel", "lat": 31.9579, "lng": 35.9344, "dwellMin": 60 },
{ "name": "Rainbow Street", "lat": 31.9510, "lng": 35.9302, "dwellMin": 45 },
{ "name": "The Boulevard Abdali", "lat": 31.9707, "lng": 35.9106, "dwellMin": 90 }
],
"costing": "auto"
}'
Scope required: routing
Request body
auto (default), bicycle, or pedestrian.Each stops[] element:
dwellMinutes total. Defaults to 0.Response
| Field | Type | Description |
|---|---|---|
provider | string | Always navvo-itinerary. |
available | boolean | true when the trip could be routed. false if no route exists between the stops. |
orderedStops[] | array | The stops in optimized visiting order (first and last unchanged). Each carries the original stop fields. |
legs[] | array | Travel legs between consecutive ordered stops. |
totals | object | Trip-level roll-up (see below). |
Each legs[] element:
| Field | Type | Description |
|---|---|---|
from | number | Index into orderedStops[] of the leg's start. |
to | number | Index into orderedStops[] of the leg's end. |
distanceKm | number | Leg distance in kilometres. |
minutes | number | Leg travel time in minutes. |
The totals object:
| Field | Type | Description |
|---|---|---|
distanceKm | number | Total distance across all legs. |
travelMinutes | number | Total time moving between stops. |
dwellMinutes | number | Sum of every stop's dwellMin. |
totalMinutes | number | travelMinutes + dwellMinutes — the end-to-end trip duration. |
{
"provider": "navvo-itinerary",
"available": true,
"orderedStops": [
{ "name": "Amman Citadel", "lat": 31.9579, "lng": 35.9344, "dwellMin": 60 },
{ "name": "Rainbow Street", "lat": 31.9510, "lng": 35.9302, "dwellMin": 45 },
{ "name": "The Boulevard Abdali", "lat": 31.9707, "lng": 35.9106, "dwellMin": 90 }
],
"legs": [
{ "from": 0, "to": 1, "distanceKm": 1.2, "minutes": 6 },
{ "from": 1, "to": 2, "distanceKm": 3.8, "minutes": 11 }
],
"totals": {
"distanceKm": 5.0,
"travelMinutes": 17,
"dwellMinutes": 195,
"totalMinutes": 212
}
}
POST /itinerary/auto
Where /itinerary/plan orders stops you already chose, /itinerary/auto discovers and picks them: give it a starting point, a time budget, and (optionally) what you're into — it finds nearby places, then selects and orders the highest-value set that fits the time you have. It's solved as an orienteering problem (prize-collecting): every candidate's "skip cost" scales with its rating, under a route-duration cap equal to your budget, so the best stops that fit are kept and the rest dropped. Scope routing.
| Field | Type | Notes |
|---|---|---|
start | {lat,lng} | required — where the day begins |
timeBudgetMin | number | total time available, minutes (default 240; 30–1440) |
interests | string | category keywords to match (e.g. ["cafe","museum"]); omit for top-rated of any kind |
radiusKm | number | search radius from start (default 8; max 50) |
dwellMin | number | minutes spent at each stop (default 30) |
maxCandidates | number | how many nearby POIs to consider (default 25; max 60) |
costing | string | travel profile (default pedestrian) |
curl -s -X POST "https://navvo.io/api/v1/itinerary/auto" \
-H "x-api-key: $NAVVO_API_KEY" -H "content-type: application/json" \
-d '{ "start": {"lat":31.9539,"lng":35.9106}, "timeBudgetMin": 180, "interests": ["cafe","museum"], "dwellMin": 30 }'
{
"provider": "navvo-itinerary", "available": true,
"start": { "lat": 31.9539, "lng": 35.9106 },
"orderedStops": [
{ "order": 1, "placeId": "…", "slug": "…", "name": "…", "category": "cafe", "rating": 4.7, "dwellMin": 30, "lat": 31.95, "lng": 35.91 }
],
"skipped": [ { "name": "…", "rating": 4.1 } ],
"totals": { "stops": 4, "candidatesConsidered": 25, "travelMinutes": 49, "dwellMinutes": 120, "totalMinutes": 169, "budgetMinutes": 180 }
}
The plan never exceeds budgetMinutes (totalMinutes = travel + dwell ≤ budget). Feed orderedStops straight into POST /me/itineraries to save it.
Saved itineraries
These endpoints let a signed-in user store, update, share, and reload their trips. Unlike the rest of this module, they operate on a user account, so they require a user session token (the JWT issued when a user signs in) rather than only an API key — pass it as Authorization: Bearer <user-jwt>. The one exception is the public share endpoint, which needs no auth at all.
POST /itinerary/plan, then saves the result with POST /me/itineraries — store the returned plan so reopening the trip is instant. Set isPublic:true to get a shareSlug you can hand out via GET /itineraries/{shareSlug}.GET /me/itineraries
List the signed-in user's saved itineraries.
curl -s "https://navvo.io/api/v1/me/itineraries" \
-H "Authorization: Bearer <user-jwt>"
Scope required: user session token (not an API key).
Response
| Field | Type | Description |
|---|---|---|
provider | string | Always navvo-itinerary. |
count | number | Number of itineraries returned. |
itineraries[] | array | The user's saved itineraries (see item shape). |
{
"provider": "navvo-itinerary",
"count": 1,
"itineraries": [
{
"id": "itn_8f21",
"name": "Amman Day Trip",
"stops": [
{ "name": "Amman Citadel", "lat": 31.9579, "lng": 35.9344 },
{ "name": "Rainbow Street", "lat": 31.9510, "lng": 35.9302 }
],
"notes": "Start late morning",
"isPublic": true,
"shareSlug": "amman-day-trip-8f21",
"createdAt": "2026-06-20T09:12:00Z",
"updatedAt": "2026-06-20T09:12:00Z"
}
]
}
POST /me/itineraries
Create a saved itinerary for the signed-in user.
curl -s "https://navvo.io/api/v1/me/itineraries" \
-H "Authorization: Bearer <user-jwt>" \
-H "Content-Type: application/json" \
-d '{
"name": "Amman Day Trip",
"stops": [
{ "name": "Amman Citadel", "lat": 31.9579, "lng": 35.9344, "dwellMin": 60 },
{ "name": "Rainbow Street", "lat": 31.9510, "lng": 35.9302, "dwellMin": 45 }
],
"notes": "Start late morning",
"isPublic": true
}'
Scope required: user session token (not an API key).
Request body
/itinerary/plan)./itinerary/plan) so the trip reopens without re-routing.true, Navvo mints a shareSlug so the trip can be viewed publicly. Defaults to false.Response
| Field | Type | Description |
|---|---|---|
provider | string | Always navvo-itinerary. |
id | string | The new itinerary id. |
name | string | The itinerary name. |
stops[] | array | The saved stops. |
notes | string | The saved notes, if any. |
plan | object | The cached plan, if one was supplied. |
isPublic | boolean | Whether the trip is publicly shareable. |
shareSlug | string | The public slug (present only when isPublic is true). |
createdAt / updatedAt | string | ISO timestamps. |
{
"provider": "navvo-itinerary",
"id": "itn_8f21",
"name": "Amman Day Trip",
"stops": [
{ "name": "Amman Citadel", "lat": 31.9579, "lng": 35.9344, "dwellMin": 60 },
{ "name": "Rainbow Street", "lat": 31.9510, "lng": 35.9302, "dwellMin": 45 }
],
"notes": "Start late morning",
"isPublic": true,
"shareSlug": "amman-day-trip-8f21",
"createdAt": "2026-06-20T09:12:00Z",
"updatedAt": "2026-06-20T09:12:00Z"
}
POST /me/itineraries/{id}
Update an existing saved itinerary. Send only the fields you want to change; the same body fields as POST /me/itineraries are accepted.
curl -s "https://navvo.io/api/v1/me/itineraries/itn_8f21" \
-H "Authorization: Bearer <user-jwt>" \
-H "Content-Type: application/json" \
-d '{
"name": "Amman Day Trip (revised)",
"isPublic": false
}'
Scope required: user session token (not an API key).
Response
Returns the updated itinerary object (same shape as POST /me/itineraries), with a refreshed updatedAt.
DELETE /me/itineraries/{id}
Delete a saved itinerary owned by the signed-in user.
curl -s -X DELETE "https://navvo.io/api/v1/me/itineraries/itn_8f21" \
-H "Authorization: Bearer <user-jwt>"
Scope required: user session token (not an API key).
Response
| Field | Type | Description |
|---|---|---|
provider | string | Always navvo-itinerary. |
deleted | boolean | true when the itinerary was removed. |
id | string | The id that was deleted. |
{
"provider": "navvo-itinerary",
"deleted": true,
"id": "itn_8f21"
}
GET /itineraries/{shareSlug}
Fetch a publicly shared itinerary by its shareSlug. This endpoint is public — no API key or user token is needed — so it's ideal for share links and read-only trip pages. Only itineraries saved with isPublic:true are reachable.
curl -s "https://navvo.io/api/v1/itineraries/amman-day-trip-8f21"
Scope required: none (public).
Response
| Field | Type | Description |
|---|---|---|
provider | string | Always navvo-itinerary. |
name | string | The itinerary name. |
stops[] | array | The trip's stops. |
notes | string | The trip notes, if any. |
plan | object | The cached plan, if one was saved. |
shareSlug | string | The public slug. |
{
"provider": "navvo-itinerary",
"name": "Amman Day Trip",
"stops": [
{ "name": "Amman Citadel", "lat": 31.9579, "lng": 35.9344, "dwellMin": 60 },
{ "name": "Rainbow Street", "lat": 31.9510, "lng": 35.9302, "dwellMin": 45 }
],
"notes": "Start late morning",
"plan": {
"totals": { "distanceKm": 1.2, "travelMinutes": 6, "dwellMinutes": 105, "totalMinutes": 111 }
},
"shareSlug": "amman-day-trip-8f21"
}
GET /geofences
List geofences, optionally filtered to a single owner. Use it to render the zones a B2B account has defined — circles and polygons you can draw on a map or feed into /geofences/evaluate.
curl -s "https://navvo.io/api/v1/geofences?ownerId=fleet-acme" \
-H "x-api-key: nvvo_YOUR_KEY"
Scope required: geo
Query parameters
Response
| Field | Type | Description |
|---|---|---|
provider | string | Always navvo-geofencing. |
count | number | Number of geofences returned. |
geofences[] | array | The geofences (see shape). |
{
"provider": "navvo-geofencing",
"count": 1,
"geofences": [
{
"id": "gf_warehouse",
"name": "Amman Warehouse",
"shape": "circle",
"lat": 31.9870,
"lng": 35.8650,
"radiusM": 250,
"webhookUrl": "https://acme.example.com/hooks/navvo",
"ownerId": "fleet-acme"
}
]
}
POST /geofences
Create a geofence — a circular or polygon zone. Attach a webhookUrl and Navvo will POST to it on enter/exit transitions detected by /geofences/evaluate.
curl -s "https://navvo.io/api/v1/geofences" \
-H "x-api-key: nvvo_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Amman Warehouse",
"shape": "circle",
"lat": 31.9870,
"lng": 35.8650,
"radiusM": 250,
"webhookUrl": "https://acme.example.com/hooks/navvo",
"ownerId": "fleet-acme"
}'
Scope required: geo
Request body
circle or polygon.shape is circle.shape is circle.shape is circle.[lng, lat] pairs. Required when shape is polygon./geofences/evaluate).Response
Returns the created geofence.
| Field | Type | Description |
|---|---|---|
provider | string | Always navvo-geofencing. |
id | string | The new geofence id. |
name | string | The geofence name. |
shape | string | circle or polygon. |
lat / lng / radiusM | number | Circle geometry (present for circle fences). |
polygon | array | Polygon ring of [lng, lat] pairs (present for polygon fences). |
webhookUrl | string | The webhook target, if set. |
ownerId | string | The owner id, if set. |
{
"provider": "navvo-geofencing",
"id": "gf_warehouse",
"name": "Amman Warehouse",
"shape": "circle",
"lat": 31.9870,
"lng": 35.8650,
"radiusM": 250,
"webhookUrl": "https://acme.example.com/hooks/navvo",
"ownerId": "fleet-acme"
}
"shape": "polygon" with a polygon ring instead of lat/lng/radiusM, e.g. "polygon": [[35.86,31.98],[35.87,31.98],[35.87,31.99],[35.86,31.99]]. Coordinate order is [lng, lat].POST /geofences/evaluate
Evaluate a device position against your geofences. Returns which fences the point is inside right now and — when you provide a deviceId — the enter/exit transitions since that device was last seen. With emit:true, Navvo also POSTs a webhook to each crossed fence's webhookUrl.
When you pass deviceId and emit, Navvo diffs the new position against the device's last recorded state to compute transitions, persists the new state, and fires a webhook for every fence that was entered or exited. Without a deviceId it performs a stateless point-in-fence check and transitions is empty.
curl -s "https://navvo.io/api/v1/geofences/evaluate" \
-H "x-api-key: nvvo_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"lat": 31.9869,
"lng": 35.8651,
"deviceId": "truck-17",
"ownerId": "fleet-acme",
"emit": true
}'
Scope required: geo
Request body
true (with a deviceId), Navvo POSTs a webhook to each crossed fence's webhookUrl. Defaults to false.Response
| Field | Type | Description |
|---|---|---|
provider | string | Always navvo-geofencing. |
lat / lng | number | The evaluated position (echoed back). |
inside[] | string | Ids of the geofences the point is currently inside. |
transitions[] | array | Enter/exit transitions since the device was last seen (empty without a deviceId). |
Each transitions[] element:
| Field | Type | Description |
|---|---|---|
geofenceId | string | The fence that was crossed. |
type | string | enter or exit. |
{
"provider": "navvo-geofencing",
"lat": 31.9869,
"lng": 35.8651,
"inside": ["gf_warehouse"],
"transitions": [
{ "geofenceId": "gf_warehouse", "type": "enter" }
]
}
emit fires, Navvo POSTs JSON to the fence's webhookUrl describing the crossing — the geofenceId, the type (enter/exit), the deviceId, the lat/lng, and an ownerId. Respond 2xx to acknowledge; Navvo retries transient failures.POST /geofences/evaluate-batch
Check many points at once against all your active fences in a single call — e.g. a whole fleet's live positions. The fences are loaded once and every point is tested against them, so it's far cheaper than one /geofences/evaluate per vehicle. It's a stateless snapshot (which fences each point is in, plus per-fence occupancy); for stateful enter/exit events, use /geofences/evaluate per device.
curl -s -X POST "https://navvo.io/api/v1/geofences/evaluate-batch" \
-H "x-api-key: $NAVVO_API_KEY" -H "content-type: application/json" \
-d '{ "points": [ {"deviceId":"v1","lat":31.9505,"lng":35.9105}, {"deviceId":"v2","lat":31.97,"lng":35.89} ] }'
| Field | Type | Notes |
|---|---|---|
points | array | up to 1000 { deviceId?, lat, lng } |
ownerId | string | optional — scope to one owner's fences (defaults to your project) |
{
"provider": "navvo-geofencing", "count": 2, "fencesChecked": 2,
"occupancy": { "gf_downtown": 1, "gf_shmeisani": 1 },
"results": [
{ "deviceId": "v1", "lat": 31.9505, "lng": 35.9105, "inside": ["gf_downtown"], "insideNamed": [ { "id": "gf_downtown", "name": "Downtown circle" } ] },
{ "deviceId": "v2", "lat": 31.97, "lng": 35.89, "inside": ["gf_shmeisani"], "insideNamed": [ { "id": "gf_shmeisani", "name": "Shmeisani box" } ] }
]
}
occupancy is a fence-id → count map (how many of your points are in each fence) — handy for a live "vehicles per zone" dashboard. Works for both circle and polygon fences.
GET /tiles/static
Render an embeddable static map image. Compose a Navvo basemap centred on a point at a given zoom, drop pin markers on it, and get back an image you can place directly in an <img> tag — no JavaScript map needed. Ideal for emails, link previews (Open Graph / social cards), and server-rendered cards where an interactive map can't run.
The image is returned as SVG (Content-Type: image/svg+xml), so it stays crisp at any size and renders directly in a browser, an <img>, or a CSS background-image.
curl -s "https://navvo.io/api/v1/tiles/static?center=31.9539,35.9106&zoom=13&size=600x400&markers=31.9579,35.9344;31.9510,35.9302&style=standard&api_key=nvvo_YOUR_KEY" \
-o map.svg
Embed it straight into a page or email — set the api_key in the query string because an <img> can't send an x-api-key header:
<img
src="https://navvo.io/api/v1/tiles/static?center=31.9539,35.9106&zoom=13&size=600x400&markers=31.9579,35.9344;31.9510,35.9302&style=standard&api_key=nvvo_YOUR_KEY"
width="600"
height="400"
alt="Map of Amman"
/>
Scope required: tiles
Query parameters
lat,lng (e.g. 31.9539,35.9106).1–19). Higher is closer in.WxH (e.g. 600x400). Defaults to a standard card size; capped server-side.lat,lng (e.g. 31.9579,35.9344;31.9510,35.9302). Each becomes a Navvo pin on the image.standard (default), dark, satellite, or terrain.Response
A map image. The response body is SVG with Content-Type: image/svg+xml, sized to your size parameter, showing the Navvo basemap centred on center at zoom with a pin for each markers entry.
<img> can't send headers, pass your key as api_key=nvvo_YOUR_KEY in the query string for static-map URLs — the same pattern used by map tile and style URLs. See Map Tiles & Styles for details.