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.

https://api.routebase.nl/v1

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.

POST /v1/day-plans sync · ≤ 120 stops · larger via "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.

queued running succeeded/ failed/ cancelled
GET /v1/jobs/{id} poll status · result embedded when succeeded

A succeeded job can still describe an infeasible plan — always read result.status (ok · ok_with_warnings · infeasible).

DELETE /v1/jobs/{id} cancel while still queued

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.

POST /v1/recurring-plans async · returns 202 + job

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.

POST /v1/detour-candidates sync · milliseconds

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 }
  ]
}
POST /v1/insertions sync · commit accepted candidates

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": [ "…" ] } }
StatusTypeMeaning
401unauthorizedMissing, malformed, or unknown API key.
422validation_errorRequest body doesn't match the schema; errors[] pinpoints each field.
422unreachable_locationsNo road route between some points (outside coverage); offending pairs listed.
422too_large_for_syncOver 120 stops — resubmit with "async": true.
422invalid_tasks / client_id_out_of_rangeRecurring tasks violate the tasks contract or index a missing location.
413Body over 32 MB.
503matrix_unavailable / db_unavailableA dependency is down; retry with backoff.
Good to know — travel matrices are in seconds (they truncate to whole minutes inside the solver) · coordinate mode covers the Benelux road network · up to 1,500 locations per recurring plan · finished jobs are retained for 30 days.

Explore

The interactive playground lets you fire real requests from the browser, and the raw OpenAPI spec generates clients in any language.