Route optimisation over a truck-aware matrix
POST /optimise (alias /optimize) is route optimisation for multi-vehicle, multi-stop fleets: which vehicle visits which stops, in what order, inside time windows and capacities. The part that matters sits underneath it. The travel-time matrix the solver plans against is computed by our own routing engine with your costing, costing_options and adr profile applied, so truck dimensions and ADR tunnel categories shape the plan itself rather than being checked once it comes back. A plan that hands a driver a stop they cannot legally reach is worse than no plan. One request bills a flat 10 calls, fair use caps a problem at 200 unique locations, and the endpoint is enabled per deployment: where an operator has not enabled it, you get a 503.
Why does the matrix underneath decide whether a plan is legal?
Because a solver only ever sees numbers, and the law lives in those numbers. A vehicle-routing solver does not know what a bridge is. It sees an N×N matrix of durations and rearranges stops to minimise a cost. If that matrix was built for a car, then every leg it reasons about is a car leg, and the plan comes back neatly sequenced and legally worthless for a 4 m artic carrying a tunnel-code load.
This breaks in two ways we have both seen:
- Optimise on car times, route on truck times. The planner sizes a matrix with no truck parameters, picks a sequence, then routes it properly. The sequence is optimal for a vehicle you do not own, and the real drive times do not match the plan the depot is working to.
- Optimise, then validate. The plan is checked afterwards and failing stops get dropped or bounced to a human. You are holding a partial plan and a phone call, which is what the optimiser existed to prevent.
The gateway never lets the solver do its own routing. It computes the full travel-time and distance matrix through its own engine, with your costing applied, and hands the solver explicit matrices. POST /optimise therefore respects exactly the same constraints as POST /route and POST /matrix. The dimensional argument is the same one we make for single routes in truck routing with dimensions enforced in costing, and the matrix that feeds this endpoint is the one described in the matrix API.
What does a request look like?
A fleet, some stops, a costing and optionally an ADR profile. Locations are { "lat": …, "lon": … } objects with named keys, so there is no coordinate-order ambiguity. Durations are seconds, distances are metres. A London depot, deliveries to Birmingham and Manchester, back to the depot, 44-tonne artic carrying hazmat with ADR tunnel code C:
export BASE=https://api.mapmap.ai
export API_KEY=snk_...
curl -fsS -X POST "$BASE/optimise" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"vehicles": [
{ "id": 1,
"start": { "lat": 51.5074, "lon": -0.1278 },
"end": { "lat": 51.5074, "lon": -0.1278 },
"capacity": [10], "time_window": [28800, 64800] }
],
"jobs": [
{ "id": 10, "location": { "lat": 52.4862, "lon": -1.8904 },
"service_s": 300, "delivery": [2] },
{ "id": 11, "location": { "lat": 53.4808, "lon": -2.2426 },
"service_s": 300, "delivery": [3] }
],
"costing": "truck",
"adr": {
"dimensions": { "height_m": 4.0, "width_m": 2.55,
"length_m": 16.5, "gross_weight_t": 40.0 },
"tunnel_code": "C",
"hazmat": true
}
}'
The top-level body:
| Field | Required | Description |
|---|---|---|
vehicles | yes | The fleet. At least one. |
jobs | see note | Single-stop jobs. At least one job or shipment is required. |
shipments | see note | Pickup and delivery pairs that ride the same vehicle, pickup first. |
costing | no | "auto" (default) or "truck". Chooses the matrix costing. |
costing_options | no | Valhalla-style options passed to the matrix verbatim, e.g. {"truck": {"height": 4.0}}. |
adr | no | ADR profile (dimensions, tunnel_code, hazmat). Requires "costing": "truck", else 400. |
eu_drivers_hours | no | Auto-generate a compliant mid-shift break. See below. |
time_window, time_windows and the response's arrival values are plain seconds on any consistent epoch you choose. The example uses seconds since midnight, so 28800 is 08:00 and 64800 is 18:00.
The response, truncated but shape-complete:
{
"code": 0,
"profile": "truck",
"summary": { "cost": 26000, "routes": 1, "unassigned": 0,
"duration": 26000, "service": 600, "waiting_time": 0,
"distance": 655000 },
"unassigned": [],
"routes": [
{ "vehicle": 1, "cost": 26000, "duration": 26000, "distance": 655000,
"service": 600, "waiting_time": 0,
"steps": [
{ "type": "start", "arrival": 28800, "duration": 0,
"service": 0, "waiting_time": 0, "load": [5],
"location": { "lat": 51.5074, "lon": -0.1278 } },
{ "type": "job", "id": 10, "arrival": 35400, "duration": 6600,
"service": 300, "waiting_time": 0, "load": [3],
"location": { "lat": 52.4862, "lon": -1.8904 } },
{ "type": "job", "id": 11, "arrival": 41000, "duration": 11900,
"service": 300, "waiting_time": 0, "load": [0],
"location": { "lat": 53.4808, "lon": -2.2426 } },
{ "type": "end", "arrival": 55400, "duration": 26000,
"service": 0, "waiting_time": 0, "load": [0],
"location": { "lat": 51.5074, "lon": -0.1278 } }
] }
]
}
Two things when you parse it. profile echoes the matrix costing actually used, "auto" or "truck", so you can assert you got the vehicle you asked for. And unassignable tasks are never silently dropped, each appearing in unassigned with its id, type and location, so a stop no vehicle can legally serve is visible rather than missing.
How does it handle driver breaks and work limits?
With explicit breaks, auto-generated EU drivers' hours, per-vehicle caps and per-vehicle costs. Fleets do not optimise for time alone: they optimise inside the law and inside a budget.
A breaks array on a vehicle is a mandatory rest with no location: the solver places it wherever the time_windows allow, and it comes back as a step of type: "break". Each break takes id, time_windows, an optional service_s duration, an optional max_load (the break is only allowed while the load is at or below it) and an optional description.
Set eu_drivers_hours at the top level and the gateway generates one for every vehicle that has a time_window but no explicit breaks:
{ "eu_drivers_hours": true }
true gives the Regulation (EC) No 561/2006 defaults: 45 minutes' rest after at most 4.5 hours' driving. Pass an object to override them, for example { "driving_before_break_s": 16200, "break_duration_s": 2700 }. A break is generated only when the shift is long enough to need one, and an explicit breaks array always wins.
The rest of the compliance and cost surface lives on the vehicle:
| Field | What it does |
|---|---|
time_window | The working window, in seconds on your epoch. |
max_travel_time | Caps driving time in seconds, excluding service, setup and waiting. A daily driving limit maps here. |
max_tasks | Caps how many tasks this vehicle may serve. |
capacity | Multidimensional capacity, matched against job delivery and pickup and shipment amount. |
skills | A task requiring a skill the vehicle lacks is never assigned to it. ADR-trained drivers and tail-lifts both model here. |
costs | {fixed, per_hour, per_task_hour, per_km}, for plans that are cost-optimal rather than merely time-optimal. |
What does an optimisation cost?
A flat 10 calls of its class per request, regardless of problem size, and the internal N×N matrix it triggers is not billed on top. The class is decided from the request: Standard for costing: "auto", Premium for truck or ADR.
| Request | Class | Entry-band price per optimisation |
|---|---|---|
"costing": "auto" | Standard, 0.05p per call | 0.5p |
"costing": "truck" or an adr profile | Premium, 1p per call | 10p |
Both classes are tiered and marginal, so each band prices only the calls inside it: Standard runs 0.05p, then 0.03p beyond 1M calls a month, then 0.02p beyond 10M. Premium runs 1p, 0.7p and 0.4p across the same bands. Above 25M a month we quote.
On the free tier, 50,000 included calls a month land after you verify an email, and commercial use is allowed. Included volume is denominated in standard calls and a premium call draws 20, so a truck optimisation draws 200 included calls and an auto one draws 10.
What it does not do
Route optimisation is off unless the operator turns it on. The solver runs as an optional sidecar, so a deployment with no SN_VROOM_URL configured returns 503 with type urn:sn-gateway:problem:optimisation-not-enabled, and there is nothing you can do about it from the client side. On self-host, the distro's docker-compose.yml ships a ready-made service to uncomment.
The other limits worth knowing before you build against this:
- 200 unique locations per request. Beyond that you get
422with typeurn:sn-gateway:problem:optimisation-too-large, and a body carryingmax_locationsandlocationsso you can size the split. Talk to us for larger sustained problems. - No geometry comes back. The solver returns sequences, not lines.
options.g: trueis a400, not a slow path. Fetch each leg fromPOST /routewhen you need something to draw. eu_drivers_hoursis a single-shift approximation. It covers a day fleet's mid-shift rest. It does not model split breaks, daily and weekly rest or duty-time-exact enforcement. For those, post-validate the returned plan. We would rather tell you the shape of the gap than let you find it at a tachograph download.- Unreachable pairs are penalised, not rejected. If a location pair cannot be reached under your costing, the matrix cell gets a very large sentinel cost instead of an error, so the solver avoids it where any alternative exists. A returned duration in the millions of seconds means a stop is probably unreachable: check its coordinates and your truck constraints.
400is yours,502is ours. Invalid input is400and never worth retrying. A502(urn:sn-gateway:problem:upstream-error) means the sidecar failed, and a retry is reasonable.
Try it
The full field reference, every error slug and the shipment shapes are in the route optimisation docs. Agents get the same capability as the optimise_routes MCP tool: check compliance with check_adr_tunnel, optimise the visits, then fetch geometry per leg. Issue a key with one card-free call and paste the curl above.
Routing derives from OpenStreetMap, so credit "© OpenStreetMap contributors" when you render or republish it.
