Documentation menu
Route optimisation
POST /optimise (alias /optimize) solves multi-vehicle, multi-stop
problems: which vehicle visits which stops, in what order, within time
windows and capacities. It is the planning layer on top of routing, and it
carries our differentiator all the way through: when you optimise with
costing: "truck" (optionally with an adr profile; ADR is the European
agreement on carriage of dangerous goods by road; tunnel codes B–E restrict
which tunnels a hazmat load may use, see conventions
for the full table), the travel-time matrix underneath is computed by our
routing engine with those dimensional and ADR constraints, so the plan
never assumes a leg a lorry cannot legally drive.
Availability
Status, honestly: the hosted gateway at https://api.mapmap.ai is live:
sign up for a key, or run the self-host distro.
Base URL and authentication conventions are on the
conventions page.
The solver itself runs as an optional sidecar on the deployment:
- Self-host: set
SN_VROOM_URLon the gateway to a vroom-express sidecar; the self-host guide and the distro'sdocker-compose.ymlinclude a ready-made service to uncomment. - When the sidecar is not configured, the endpoint returns
503with problem typeurn:sn-gateway:problem:optimisation-not-enabled.
The gateway never lets the solver do its own routing: it computes the full
travel-time/distance matrix through its own engine (with your costing,
costing_options and adr profile applied) and hands the solver explicit
matrices. The optimisation respects exactly the same constraints as
/route and /matrix.
Request
You need an API key (Authorization: Bearer snk_…; get one in the
quickstart). Locations on this endpoint are
{ "lat": …, "lon": … } objects with named keys, so there is no
coordinate-order ambiguity. Durations are seconds, distances metres.
Top-level body
| Field | Type | Required | Description |
|---|---|---|---|
vehicles | array | yes (≥ 1) | The fleet. |
jobs | array | see note | Single-stop jobs. At least one job or shipment is required. |
shipments | array | see note | Pickup+delivery pairs that must ride the same vehicle, pickup first. |
costing | string | no (default "auto") | Costing model for the travel-time matrix: "auto" or "truck". |
costing_options | object | no | Valhalla-style costing options passed to the matrix verbatim, e.g. {"truck": {"height": 4.0}}. |
adr | object | no | ADR vehicle profile, same shape as on POST /route (dimensions, tunnel_code, hazmat). Requires "costing": "truck" (else 400); merged into costing_options.truck. |
options | object | no | Only g (geometry) exists, and it is not supported: {"g": true} is a 400. Fetch leg geometry via POST /route instead. |
Vehicles
| Field | Type | Required | Description |
|---|---|---|---|
id | integer | yes | Caller-chosen id, echoed back on the vehicle's route. |
start | {lat, lon} | at least one of start/end | Start location. |
end | {lat, lon} | at least one of start/end | End location; omitted, the route ends at its last stop. |
capacity | integer array | no | Multidimensional capacity; same length as job delivery/pickup and shipment amount. |
skills | integer array | no | Skills this vehicle provides; a task requiring a skill the vehicle lacks is never assigned to it. |
time_window | [start, end] | no | Working window in seconds (see "Times" below). |
breaks | array | no | Mandatory rest breaks (see "Driver hours & compliance"). |
max_travel_time | integer | no | Cap on driving time in seconds (excludes service, setup and waiting). A daily driving limit maps here. |
max_tasks | integer | no | Cap on the number of tasks this vehicle may serve. |
costs | object | no | Cost model, {fixed, per_hour, per_task_hour, per_km}, for cost-optimal (not just time-optimal) plans. Any omitted field uses the solver default. |
Jobs
| Field | Type | Required | Description |
|---|---|---|---|
id | integer | yes | Caller-chosen id, echoed back in steps and unassigned. |
location | {lat, lon} | yes | Job location. |
service_s | integer | no | On-site service time in seconds. |
delivery | integer array | no | Quantities delivered to the job (matches vehicle capacity). |
pickup | integer array | no | Quantities picked up at the job. |
skills | integer array | no | Skills the job requires. |
time_windows | array of [start, end] | no | Acceptable arrival windows in seconds. |
Shipments
| Field | Type | Required | Description |
|---|---|---|---|
pickup | object | yes | The pickup end: id, location, optional service_s, optional time_windows (same shapes as on jobs). |
delivery | object | yes | The delivery end, same shape as pickup. |
amount | integer array | no | Quantities moved (matches vehicle capacity). |
skills | integer array | no | Skills the shipment requires. |
Times
time_window, time_windows and the response's arrival values are plain
seconds on any consistent epoch you choose. The example below uses
seconds since midnight: 28800 = 08:00, 64800 = 18:00. Arrivals in the
response come back on the same scale.
Driver hours & compliance
Fleets do not just optimise for time; they optimise inside the law. Two mechanisms, both of which the underlying solver does not model on its own:
Explicit breaks. Give a vehicle a breaks array. Each break is a
mandatory rest with no location: the solver inserts it into the route
wherever the windows allow:
| Field | Type | Required | Description |
|---|---|---|---|
id | integer | yes | Echoed back on the break step. |
time_windows | array of [start, end] | yes | When the break may be taken (same epoch as time_window). |
service_s | integer | no | Break duration in seconds. |
max_load | integer array | no | The break is only allowed while the load is at or below this. |
description | string | no | Free text, echoed back on the step. |
EU drivers' hours (auto-generated). Set eu_drivers_hours at the top
level to have the gateway generate a compliant break for every vehicle that
has a time_window but no explicit breaks:
{ "eu_drivers_hours": true }
Pass true for the Regulation (EC) No 561/2006 defaults (45 minutes' rest
after at most 4.5 hours' driving) or an object to override them:
{ "eu_drivers_hours": { "driving_before_break_s": 16200, "break_duration_s": 2700 } }
A break is generated only when the shift is long enough to need one; short
shifts and vehicles without a time_window are left alone, and an explicit
breaks array always takes precedence. This is a single-shift
approximation: it covers a day fleet's mid-shift rest, but does not model
split breaks, daily/weekly rest, or duty-time-exact enforcement; for those,
post-validate the returned plan. Breaks appear in the route as steps of
type: "break".
Example
A London depot, deliveries in 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
}
}'
Response
Truncated but shape-complete: every field shown here is always present on
a 200 (except distance and load, which appear when the matrix carries
distances and the problem carries quantities, and id, which only job,
pickup and delivery steps have):
{
"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 } }
] }
]
}
Reading it:
codeis always0on HTTP 200; solver failures surface as HTTP errors, never as a non-zero code in a 200 body.profileechoes the matrix costing used:"auto"or"truck".costis the solver's objective; with the matrices the gateway supplies it equals travel time in seconds.- Step
durationis cumulative travel time up to that step;arrivalis on your chosen epoch (see "Times" above). - Unassignable tasks are never silently dropped: each appears in
unassignedas{ "id": 12, "type": "job", "location": { "lat": …, "lon": … } }. - Route geometry is not returned by the solver; request each leg from
POST /routeif you need lines on a map.
Unreachable pairs: if a location pair is unreachable under the chosen costing, the matrix cell is penalised with a very large sentinel cost rather than an error, so the solver avoids it whenever any alternative exists. If a returned duration looks absurdly large (millions of seconds), a stop is likely unreachable; check its coordinates and your truck constraints.
Errors
Errors are application/problem+json (see
conventions for the envelope and the 402-vs-429
distinction). The one that matters for retry logic: your input's fault is
400, the solver's fault is 502.
| Status | type URN | Meaning | Retry? |
|---|---|---|---|
| 400 | urn:sn-gateway:problem:bad-request | Invalid problem: no vehicles; no jobs or shipments; a vehicle with neither start nor end; an invalid coordinate; options.g: true; adr without "costing": "truck"; or the solver rejected the input. | No; fix the request. |
| 401 | urn:sn-gateway:problem:unauthorized | Missing or invalid API key. | No. |
| 422 | urn:sn-gateway:problem:optimisation-too-large | More than 200 unique locations (fair-use cap). Body carries max_locations and locations. | No; split the problem. |
| 429 | urn:sn-gateway:problem:quota-exceeded or …:rate-limited | Monthly quota or rate limit; rate-limited bodies carry retry_after (seconds). | After retry_after. |
| 502 | urn:sn-gateway:problem:upstream-error | The solver sidecar is unreachable or failed. | Yes; transient. |
| 503 | urn:sn-gateway:problem:optimisation-not-enabled | No solver sidecar on this deployment (SN_VROOM_URL unset). | Not until the operator enables it. |
Example 422:
{
"type": "urn:sn-gateway:problem:optimisation-too-large",
"title": "Optimisation problem too large",
"status": 422,
"detail": "optimisation problem too large: 240 unique locations exceeds the fair-use cap of 200 per request; split the problem or contact the operator",
"max_locations": 200,
"locations": 240
}
Metering and fair use
One optimisation request bills a flat 10 calls of its class (Standard for
costing: "auto", Premium for truck/ADR) regardless of problem size; it
triggers an internal N×N matrix and a solver run, and that internal matrix
is not billed separately. See pricing for the per-class rates.
Fair use is capped at 200 unique locations per request (422 beyond); talk
to sales for larger sustained problems.
For agents
The same capability is exposed as the optimise_routes
MCP tool, so an agent can plan a fleet's day end to end: check
compliance with check_adr_tunnel, optimise the visits, then fetch
geometries per leg.
Next steps
- Conventions: base URL, auth, error envelope, ADR tunnel codes
- Analysis APIs: the matrix endpoint that powers optimisation
- Self-host: enable the solver sidecar with
SN_VROOM_URL - MCP server:
optimise_routesandcheck_adr_tunnelfor agents