50,000 free calls a month, card-free. Get an API key →

Documentation menu
docs / optimisation · raw .md

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_URL on the gateway to a vroom-express sidecar; the self-host guide and the distro's docker-compose.yml include a ready-made service to uncomment.
  • When the sidecar is not configured, the endpoint returns 503 with problem type urn: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

FieldTypeRequiredDescription
vehiclesarrayyes (≥ 1)The fleet.
jobsarraysee noteSingle-stop jobs. At least one job or shipment is required.
shipmentsarraysee notePickup+delivery pairs that must ride the same vehicle, pickup first.
costingstringno (default "auto")Costing model for the travel-time matrix: "auto" or "truck".
costing_optionsobjectnoValhalla-style costing options passed to the matrix verbatim, e.g. {"truck": {"height": 4.0}}.
adrobjectnoADR vehicle profile, same shape as on POST /route (dimensions, tunnel_code, hazmat). Requires "costing": "truck" (else 400); merged into costing_options.truck.
optionsobjectnoOnly g (geometry) exists, and it is not supported: {"g": true} is a 400. Fetch leg geometry via POST /route instead.

Vehicles

FieldTypeRequiredDescription
idintegeryesCaller-chosen id, echoed back on the vehicle's route.
start{lat, lon}at least one of start/endStart location.
end{lat, lon}at least one of start/endEnd location; omitted, the route ends at its last stop.
capacityinteger arraynoMultidimensional capacity; same length as job delivery/pickup and shipment amount.
skillsinteger arraynoSkills this vehicle provides; a task requiring a skill the vehicle lacks is never assigned to it.
time_window[start, end]noWorking window in seconds (see "Times" below).
breaksarraynoMandatory rest breaks (see "Driver hours & compliance").
max_travel_timeintegernoCap on driving time in seconds (excludes service, setup and waiting). A daily driving limit maps here.
max_tasksintegernoCap on the number of tasks this vehicle may serve.
costsobjectnoCost 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

FieldTypeRequiredDescription
idintegeryesCaller-chosen id, echoed back in steps and unassigned.
location{lat, lon}yesJob location.
service_sintegernoOn-site service time in seconds.
deliveryinteger arraynoQuantities delivered to the job (matches vehicle capacity).
pickupinteger arraynoQuantities picked up at the job.
skillsinteger arraynoSkills the job requires.
time_windowsarray of [start, end]noAcceptable arrival windows in seconds.

Shipments

FieldTypeRequiredDescription
pickupobjectyesThe pickup end: id, location, optional service_s, optional time_windows (same shapes as on jobs).
deliveryobjectyesThe delivery end, same shape as pickup.
amountinteger arraynoQuantities moved (matches vehicle capacity).
skillsinteger arraynoSkills 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:

FieldTypeRequiredDescription
idintegeryesEchoed back on the break step.
time_windowsarray of [start, end]yesWhen the break may be taken (same epoch as time_window).
service_sintegernoBreak duration in seconds.
max_loadinteger arraynoThe break is only allowed while the load is at or below this.
descriptionstringnoFree 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:

json
{ "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:

json
{ "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:

sh
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):

json
{
  "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:

  • code is always 0 on HTTP 200; solver failures surface as HTTP errors, never as a non-zero code in a 200 body.
  • profile echoes the matrix costing used: "auto" or "truck".
  • cost is the solver's objective; with the matrices the gateway supplies it equals travel time in seconds.
  • Step duration is cumulative travel time up to that step; arrival is on your chosen epoch (see "Times" above).
  • Unassignable tasks are never silently dropped: each appears in unassigned as { "id": 12, "type": "job", "location": { "lat": …, "lon": … } }.
  • Route geometry is not returned by the solver; request each leg from POST /route if 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.

Statustype URNMeaningRetry?
400urn:sn-gateway:problem:bad-requestInvalid 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.
401urn:sn-gateway:problem:unauthorizedMissing or invalid API key.No.
422urn:sn-gateway:problem:optimisation-too-largeMore than 200 unique locations (fair-use cap). Body carries max_locations and locations.No; split the problem.
429urn:sn-gateway:problem:quota-exceeded or …:rate-limitedMonthly quota or rate limit; rate-limited bodies carry retry_after (seconds).After retry_after.
502urn:sn-gateway:problem:upstream-errorThe solver sidecar is unreachable or failed.Yes; transient.
503urn:sn-gateway:problem:optimisation-not-enabledNo solver sidecar on this deployment (SN_VROOM_URL unset).Not until the operator enables it.

Example 422:

json
{
  "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_routes and check_adr_tunnel for agents