Route optimization,
one HTTP call away.
Routebase plans vehicle routes: a single optimal round-trip, a full multi-vehicle day, a recurring multi-week field schedule — or the extra pickups worth adding to a route you already committed to.
Authentication
Every endpoint except GET /v1/health requires your API key as a bearer token.
curl https://api.routebase.nl/v1/health
# {"ok": true, "db": "ok", "solver": "ok", "osrm": "ok"}
curl https://api.routebase.nl/v1/day-plans \
-H "Authorization: Bearer rb_your_api_key" \
-H "Content-Type: application/json" \
-d @request.json
Day planning
Give us stops and a depot — get back optimized routes with per-stop timing. One vehicle makes it a single optimal route; more vehicles make it a day plan. Send coordinates and we compute travel times on the road network, or bring your own travel-time matrix.
"async": true
Assigns stops to vehicles and orders each route to minimize travel, respecting a per-route
duration cap. Set "vehicles": 1 for the single-route case.
{
"depot": { "location": { "lat": 52.1601, "lng": 4.4970 } },
"stops": [
{ "id": "delft", "location": { "lat": 52.0116, "lng": 4.3571 }, "service_minutes": 45 },
{ "id": "gouda", "location": { "lat": 52.0115, "lng": 4.7105 }, "service_minutes": 30 }
],
"vehicles": 2,
"max_route_minutes": 480,
"time_budget_seconds": 5
}
Response
{
"feasible": true,
"routes": [{
"vehicle": 0,
"stops": [
{ "seq": 0, "id": "delft", "arrival_min": 23, "departure_min": 68, "travel_from_prev_min": 23 },
{ "seq": 1, "id": "gouda", "arrival_min": 101, "departure_min": 131, "travel_from_prev_min": 33 }
],
"travel_minutes": 95, "service_minutes": 75, "total_minutes": 170, "overtime_minutes": 0
}],
"metrics": { "used_vehicles": 1, "travel_minutes": 95, "total_minutes": 170, "wall_ms": 812 }
}
Jobs
Anything that takes longer than a moment runs as a job: submit, get a
job_id back immediately, poll until it finishes.
A succeeded job can still describe an infeasible plan — always read
result.status (ok ·
ok_with_warnings · infeasible).
Returns 409 once the job is already running or finished.
Recurring field planning
The flagship. For work that repeats — weekly, bi-weekly, monthly — Routebase builds a multi-week schedule: which weekday and which week each task runs, and the vehicle routes for every day. Frequencies stay honored, workload stays balanced, weekday patterns stay consistent across planning periods.
Tasks follow the published tasks contract: each task carries its week,
allowed days, duration, and a client_id that indexes the location list
(depot last). Optional inputs carry continuity between planning periods: locked weekday patterns
(phases), fixed person assignments (persons),
per-day fleet overrides (vehicle_calendar).
{
"tasks": [ { "task_id": "t1", "client_id": 0, "week": 1, "duration": 30,
"day_options": [{ "date": "2026-04-06", "weekday": 0 }], "…": "…" } ],
"locations": [ { "lat": 52.01, "lng": 4.36 }, { "lat": 52.16, "lng": 4.50 } ],
"depot_index": 1,
"vehicles": { "per_day": 6, "max_route_minutes": 570 },
"options": { "effort": "balanced" }
}
Response — 202, then poll the job
{ "job_id": "7c592ddb-…", "status": "queued", "poll": "/v1/jobs/7c592ddb-…" }
# GET /v1/jobs/{id} once succeeded → result: a full plan
{
"status": "succeeded",
"result": {
"schema_version": "result.v2",
"status": "ok",
"horizon": { "min_date": "2026-04-06", "max_date": "2026-04-17", "days": 10 },
"plan": { "days": [ { "date": "2026-04-06", "routes": [ "…" ] } ] },
"metrics": { "…": "…" }
}
}
options.effort is the quality dial:
fast (default) · balanced ·
max — higher explores more alternative schedules.
Plan together
You've committed a route — a truck going from A to B. Who along the way is worth a call? Rank nearby candidates by the minutes they'd actually add, then commit the ones that say yes.
For each candidate: the cheapest place to slot it into the fixed route, the true marginal cost in minutes, and whether it still fits under the route's time cap.
{
"backbone": {
"stops": [
{ "id": "amsterdam", "location": { "lat": 52.3600, "lng": 4.8852 }, "service_minutes": 30 },
{ "id": "antwerp", "location": { "lat": 51.2194, "lng": 4.4025 }, "service_minutes": 30 }
],
"max_route_minutes": 480
},
"candidates": [
{ "id": "breda", "location": { "lat": 51.5719, "lng": 4.7683 }, "service_minutes": 15 },
{ "id": "rotterdam", "location": { "lat": 51.9225, "lng": 4.4792 }, "service_minutes": 20 }
]
}
Response — ranked by detour
{
"backbone": { "travel_minutes": 125.8, "total_minutes": 185.8 },
"candidates": [
{ "id": "breda", "detour_minutes": 12.1, "added_minutes": 27.1, "insert_after": "amsterdam", "feasible": true },
{ "id": "rotterdam", "detour_minutes": 21.5, "added_minutes": 41.5, "insert_after": "amsterdam", "feasible": true }
]
}
The commit step: once candidates say yes, insert them. Same backbone and candidates as
/v1/detour-candidates, plus insert — the ids
to add. Each is placed at its cheapest position (recomputed after every commit) while the backbone's
own order stays fixed. Returns the full re-timed route.
{
"backbone": {
"stops": [
{ "id": "amsterdam", "location": { "lat": 52.3600, "lng": 4.8852 }, "service_minutes": 30 },
{ "id": "antwerp", "location": { "lat": 51.2194, "lng": 4.4025 }, "service_minutes": 30 }
],
"max_route_minutes": 480
},
"candidates": [
{ "id": "breda", "location": { "lat": 51.5719, "lng": 4.7683 }, "service_minutes": 15 },
{ "id": "rotterdam", "location": { "lat": 51.9225, "lng": 4.4792 }, "service_minutes": 20 }
],
"insert": ["breda", "rotterdam"]
}
Response — the committed route, minute by minute
{
"stops": [
{ "seq": 0, "id": "amsterdam", "arrival_min": 0, "departure_min": 30.0, "inserted": false },
{ "seq": 1, "id": "rotterdam", "arrival_min": 96.7, "departure_min": 116.7, "inserted": true, "added_minutes": 42.1 },
{ "seq": 2, "id": "breda", "arrival_min": 160.5, "departure_min": 175.5, "inserted": true, "added_minutes": 27.1 },
{ "seq": 3, "id": "antwerp", "arrival_min": 225.0, "departure_min": 255.0, "inserted": false }
],
"totals": { "travel_minutes": 160.0, "total_minutes": 255.0, "feasible": true }
}
Errors
Every error is JSON with a stable type:
{ "error": { "type": "unreachable_locations", "detail": "…", "pairs": [ "…" ] } }
| Status | Type | Meaning |
|---|---|---|
| 401 | unauthorized | Missing, malformed, or unknown API key. |
| 422 | validation_error | Request body doesn't match the schema; errors[] pinpoints each field. |
| 422 | unreachable_locations | No road route between some points (outside coverage); offending pairs listed. |
| 422 | too_large_for_sync | Over 120 stops — resubmit with "async": true. |
| 422 | invalid_tasks / client_id_out_of_range | Recurring tasks violate the tasks contract or index a missing location. |
| 413 | — | Body over 32 MB. |
| 503 | matrix_unavailable / db_unavailable | A dependency is down; retry with backoff. |
Explore
The interactive playground lets you fire real requests from the browser, and the raw OpenAPI spec generates clients in any language.