Skip to content

Core Concepts

Every request has two required arrays:

  • vehicles — the fleet. Each vehicle represents one route. Vehicles have a start location (the depot), optional end location (defaults to start), optional capacity, and optional constraints (time windows, skills, max distance).
  • jobs — the stops to visit. Each job has a location and optional demand, service_duration, time_windows, and skills.

The depot is not a special entity — it’s simply the vehicle’s start (and end) coordinate. There’s no depot_id or shared depot field.

All coordinates are [longitude, latitude] arrays following the GeoJSON convention. Longitude comes first.

{ "start": [-0.1278, 51.5074] }

Capacity is multi-dimensional. A vehicle with capacity: [100, 50] has two bins; jobs must fit within both. For standard single-dimension routing, use a single-element array: capacity: [100], demand: [10].

Set capacity on vehicles and demand on jobs. The solver finds routes that serve all jobs without exceeding any vehicle’s capacity.

Set time_windows on vehicles (working hours) and/or jobs (service windows). All times use ISO 8601 with timezone:

{
"time_windows": [{ "start": "2024-01-15T09:00:00Z", "end": "2024-01-15T11:00:00Z" }]
}

service_duration (seconds) is the time spent at a job stop.

When time windows are active, the response includes arrival and departure ISO 8601 timestamps on each stop.

One vehicle, no capacity constraint. The solver finds the shortest tour through all jobs.

Vehicles and jobs can both carry skills (string labels). A job with skills: ["refrigerated"] can only be served by a vehicle that also has "refrigerated" in its skills list. Unmatched jobs appear in unassigned with reason: "SKILLS_MISMATCH".

The solver needs pairwise distances and travel times between all locations. Two options:

  1. OSRM (default): omit options.matrix. The solver calls a routing engine automatically using the coordinates in your request.
  2. Custom matrix: supply options.matrix.distances (metres) and options.matrix.durations (seconds) as n×n arrays, where n = the number of unique locations (vehicle starts/ends + job locations, deduplicated by coordinate).
{
"summary": { ... },
"routes": [ ... ],
"unassigned": [ ... ]
}

Fleet-wide totals and solver stats:

FieldDescription
statusAlways "completed" for a successful run
total_distanceSum of all route distances (metres)
total_durationSum of all route travel times (seconds)
total_costSum of all vehicle costs
vehicles_usedNumber of vehicles with at least one stop
jobs_assignedJobs successfully routed
jobs_unassignedJobs that couldn’t be placed
elapsed_msActual solver wall-clock time
iterationsALNS iterations completed

One entry per vehicle used. Each route has:

  • stops — ordered stops (depot is implicit, not listed). Each stop has job_id, location, load_after, and optionally arrival/departure (ISO 8601) when time windows are active.
  • distance — route distance in metres
  • duration — route travel time in seconds
  • cost — vehicle’s fixed cost + distance × per_km rate

Each unassigned job has a reason code:

ReasonMeaning
CAPACITY_EXCEEDEDJob demand exceeds every vehicle’s remaining capacity
TIME_WINDOW_VIOLATEDNo vehicle can reach the job within its time window
SKILLS_MISMATCHNo vehicle has all required skills
VEHICLE_RANGE_EXCEEDEDAdding job would exceed vehicle’s max distance
VEHICLE_LIMIT_EXHAUSTEDAll vehicle capacity is used; one more vehicle would help
ALL_ROUTES_BLOCKEDEvery route has a mix of capacity and/or TW conflicts

The solver minimises a lexicographic objective:

  1. Unassigned jobs — always minimised first (hard constraint)
  2. Vehicles used × 1,000,000 (default weight)
  3. Total route distance × 1 (default weight)

Override the weights via options.objectives.vehicle_cost and options.objectives.distance_cost.

OptionDefaultDescription
time_limit10Solver budget in seconds
seed42RNG seed for reproducible results
matrixCustom distance/duration matrix
objectivesOverride objective weights