Route Optimization
Route Optimization
Navvo Optimize solves the hard combinatorial planning problems behind delivery, field-service and logistics operations: in what order, and on which vehicle, should a set of stops be visited to minimize total drive time — subject to vehicle capacity, customer time windows, pickup-before-delivery precedence, and multiple depots.
It is powered by a self-hosted OR-Tools solver fronted by Navvo. Travel-time matrices are built from the Navvo routing engine, so the optimizer reasons over real road network drive times — not straight-line distance. Because it is self-hosted there is no per-call optimization fee.
Scope required: optimize
{ lat, lng }.GET /optimize/health
Liveness probe for the optimization service. Use it before submitting large jobs.
{ "provider": "navvo-optimize", "available": true, "ok": true,
"service": "navvo-optimize", "solvers": ["routing", "cp-sat"] }
When available is false, submit work as an async job (see Optimization-as-a-job) and retry, or fall back to your own ordering.
POST /optimize/routes
The core Vehicle Routing Problem solver (CVRP / VRPTW). stops[0] is the depot; the remaining stops are the visits to sequence across numVehicles.
| Field | Type | Notes |
|---|---|---|
stops | {lat,lng}[] | required — index 0 is the depot, 2–500 stops |
numVehicles | number | default 1, max 200 |
costing | string | matrix profile: auto (default), truck, bicycle, pedestrian |
tier | string | fast · balanced (default) · best — more search time = better routes |
demands | number | per-stop load (same length as stops) |
vehicleCapacities | number | per-vehicle capacity (enables CVRP) |
timeWindows | [earliest,latest][] | per-stop service window, seconds (enables VRPTW) |
serviceTimes | number | per-stop dwell time, seconds |
pickupsDeliveries | [pickupIdx,deliveryIdx][] | enforces pickup-before-delivery on the same vehicle |
dropPenalty | number | allow the solver to skip a stop at this cost (handles infeasible loads) |
maxRouteDuration | number | per-vehicle cap, seconds |
openEnded | boolean | vehicles finish at their last stop instead of returning to the depot |
curl -X POST https://api.navvo.io/v1/optimize/routes \
-H 'authorization: Bearer <API_KEY>' -H 'content-type: application/json' \
-d '{
"stops": [
{"lat":31.9539,"lng":35.9106},
{"lat":31.9700,"lng":35.9200},
{"lat":31.9400,"lng":35.8800},
{"lat":31.9800,"lng":35.9300}
],
"numVehicles": 1, "tier": "balanced"
}'
{
"provider": "navvo-optimize",
"feasible": true,
"objective": { "totalDurationSec": 1840, "vehiclesUsed": 1 },
"dropped": [],
"routes": [
{ "vehicle": 0, "durationSec": 1840,
"order": [0, 2, 1, 3],
"stops": [ {"lat":31.9539,"lng":35.9106}, {"lat":31.94,"lng":35.88}, "…" ] }
]
}
order is the visit sequence as indices into your stops array; stops is the same sequence resolved back to coordinates. Any stop the solver chose to skip (only possible when dropPenalty is set) appears in dropped.
POST /optimize/reoptimize
Live mid-day re-planning. Given each vehicle's current position and the stops still to be served, it re-plans the remainder as open-ended routes (vehicles finish at their last drop).
| Field | Type | Notes |
|---|---|---|
vehiclePositions | {lat,lng}[] | required — one current position per active vehicle |
remainingStops | {lat,lng}[] | required — stops not yet served, combined with vehiclePositions up to 500 |
costing | string | matrix profile, default auto |
tier | string | default balanced |
demands | number | per-remaining-stop load |
vehicleCapacities | number | per-vehicle remaining capacity |
dropPenalty | number | allow skipping a stop |
{
"provider": "navvo-optimize", "feasible": true,
"objective": { "totalDurationSec": 920, "vehiclesUsed": 2 },
"dropped": [],
"routes": [ { "vehicle": 0, "durationSec": 540, "stops": [ "…remaining stops in order…" ] } ]
}
The vehicle's current position is stripped from the returned stops — you only get the remaining stops in their new order.
POST /optimize/binpack
CP-SAT bin packing — assign weighted items to capacity-limited containers (which parcels fit which vehicle), minimizing the number of containers used.
| Field | Type | Notes |
|---|---|---|
itemWeights | number | required — weight of each item |
binCapacities | number | required — capacity of each available bin |
curl -X POST https://api.navvo.io/v1/optimize/binpack \
-H 'authorization: Bearer <API_KEY>' -H 'content-type: application/json' \
-d '{ "itemWeights": [3,5,2,8], "binCapacities": [10,10] }'
{ "feasible": true, "assignment": [1, 1, 1, 0], "binsUsed": 2 }
assignment[i] is the bin index chosen for item i. feasible:false means the items cannot fit in the given bins (e.g. total weight exceeds total capacity).
POST /optimize/roster
CP-SAT driver rostering / shift scheduling. Assigns drivers to day/shift slots so every shift's minimum staffing is met, while balancing load fairly and respecting availability and rest rules.
| Field | Type | Notes |
|---|---|---|
numDrivers | number | required |
numDays | number | required — planning horizon, e.g. 7 |
shiftsPerDay | number | default 2 (e.g. morning / evening) |
demand | number[][] | required — demand[day][shift] = min drivers needed |
maxShiftsPerDriver | number | cap on shifts per driver in the window |
minRestDays | number | minimum days off per driver |
forbidden | [driver,day][] | unavailable slots (leave / weekly rest) |
{
"feasible": true,
"shifts": [ { "driver": 0, "day": 0, "shift": 0 }, "…" ],
"perDriver": [5, 4, 5, 4],
"peakLoad": 5
}
peakLoad is the busiest driver's total shifts — the solver minimizes it to keep the roster fair.
Optimization-as-a-job
Large problems (best tier, many stops) can take 20s or more — too long for a synchronous request. Submit them as a job: you get a jobId back instantly, then poll for the result.
POST /optimize/jobs
Enqueue any solve as a background job. The body is the same as the corresponding synchronous endpoint, plus a kind selector.
| Field | Type | Notes |
|---|---|---|
kind | string | routes (default) · reoptimize · roster · binpack |
… | — | all other fields are the body of the matching endpoint above |
curl -X POST https://api.navvo.io/v1/optimize/jobs \
-H 'authorization: Bearer <API_KEY>' -H 'content-type: application/json' \
-d '{ "kind": "routes", "tier": "best", "numVehicles": 5, "stops": [ "…" ] }'
{ "jobId": "71005767-63e7-4dbc-af7a-95c10aa18864", "status": "running", "kind": "routes" }
GET /optimize/jobs/:id
Poll a job. While running, status is running; on completion it is completed with a result (the same payload the synchronous endpoint returns), or failed with an error.
{
"id": "71005767-63e7-4dbc-af7a-95c10aa18864",
"kind": "routes", "status": "completed",
"createdAt": 1782572676434, "finishedAt": 1782572678901,
"result": { "feasible": true, "routes": [ "…" ], "objective": { "totalDurationSec": 1840 } }
}
An unknown id returns { "error": "job not found", "status": "unknown" }. Jobs are transient (in-memory) — poll until done and persist the result on your side; do not rely on a job surviving indefinitely.
fast tier with one vehicle returns in about 5 seconds; the
same 200 stops at balanced tier across 5 vehicles takes about 9 seconds. Both are fine to call
synchronously — an HTTP client just needs a timeout well above a few seconds, not milliseconds.
Reserve jobs for the best tier or the upper end of the stop range, where solve time is
dominated by search quality rather than by building the travel-time matrix.Quality tiers
| Tier | Search time | Use for |
|---|---|---|
fast | ~2s | live re-planning, small problems, interactive UIs |
balanced | ~6s | the default — good routes for daily planning |
best | ~20s | overnight / batch planning of large fleets via a job |
Related
- Fleet & Logistics guide — end-to-end delivery operations using the optimizer.
- Routing — the engine that powers the travel-time matrices.