jobs and vehicles.
This page describes the request/response schema for
POST /v3/routing/solve,
which /v3/routing/evaluate and /v3/routing/suggest also build on. The
request is strict-schema throughout: an old field name, a typo, or a feature
this page marks unsupported gets an immediate 422 (unknown field) or a
field-named 400, never a silent no-op.Design principles
Few orthogonal primitives
Eligibility, skills, and sequencing are each a single field, not five
overlapping ones. A pickup-then-delivery job is a distinct
shipments[]
entry type, not a flag on a job.Everything soft is money
No abstract weight vector. Lateness costs a rate per hour, dropping a task
costs a fee, a non-preferred vehicle costs an amount — one unit you trade
against, with an itemised cost breakdown in every response.
Penalties live on the thing
A task’s drop price (
drop_fee) and preferences sit on the task object
itself — read one job and you see exactly what it costs to bend each rule
for it.Strict schema, named rejections
Unknown fields are
422s. Fields the solver doesn’t honor yet are
field-named 400s. There is no silently-ignored option.A request at a glance
The smallest valid request — minimise travel cost, serve everything:jobs, shipments, vehicles,
relations, objective, options, and initial_routes (a warm-start —
see RouteAssignment). Only jobs
(or shipments) and vehicles are required.
Jobs
A job is a unit of work at one location — a delivery, pickup, service call, or inspection. Its visit fields (location, service_duration_s,
time_windows) sit directly on the object. A distinct pickup-then-delivery
entry type, shipments[], is covered below.
Place
Where the job is performed:
{ "coordinate": [lon, lat] } (GeoJSON order),
or the bare [lon, lat] array as shorthand. Omit location for a
virtual (location-less) job — it still has service duration, time
windows, demand, and skills, but requires no travel. There is no
location_id or depot reference form: Place is inline-coordinate only.integer
Seconds paid once on arrival at a new location, distinct from
service_duration_s and skipped when the immediate predecessor in the
route is already at this location. Not scaled by service_factor.integer
Nominal on-site service time in seconds. The serving vehicle’s
service_factor scales it, so the response’s stops[].service_s may
differ from the value you sent.array
One or more
{ "from", "to" } windows (ISO 8601). Hard by default —
arriving outside every window makes the job unassignable. An optional
per-window cost grades preference among a task’s own alternative
windows (not a soft-lateness mechanism — see
Constraints).object
Named multi-dimensional load, e.g.
{ "weight": 30, "volume": 2 }. Matched
against vehicle.capacity by name — order never matters, and a demand
dimension no vehicle declares is a 400.boolean
default:"true (implicit)"
Omitted or
true = must serve, infeasible if it cannot be. false is only
meaningful together with drop_fee — a droppable task with no price is
rejected with 400, since an optional task with nothing at stake would
never need serving.number
What leaving this task unserved costs, in the objective’s currency. The
only way to make a task droppable: it trades directly against travel cost
and is served whenever serving it is cheaper than the fee. Replaces the
removed
unassigned_cost field, which ranked drops in a tier that never
actually traded against money.array
Hard capability requirements:
[{ "name": "fridge" }]. The serving
vehicle must provide every named skill (subset match). There is no soft
(violation_cost) form on skills[] — for a soft affinity use tags +
preferred_tags (vehicle-level) or preferences (below).object
Hard filter:
allowed (whitelist, omit = all vehicles eligible) or
excluded (blacklist), mutually exclusive.string
Hard-pins the job to one vehicle id; survives re-optimisation. Stronger
than
eligible_vehicles (a set filter) and hard, unlike preferences.array
Soft vehicle affinity:
[{ "vehicle": "v2", "violation_cost": 800 }].
Each entry prices independently — a job listing several preferred
vehicles pays every entry whose vehicle didn’t serve it.array of strings
Free-form labels used for relation
group matching and, when a shift
scopes itself via serves_tags (hard) or a vehicle prices preferred_tags
(soft), for tag matching. ANY-overlap, unlike the ALL-subset skills rule.object
Soft point deadline:
{ "at": "...", "per_late_hour": 60.0 }. Arriving
after at is priced, never infeasible, and never forces waiting for early
arrival. Independent of objective.costs.per_late_hour. Not yet supported
on shipments[] (400).string (ISO 8601 instant)
Soft preference for which calendar day (UTC) a multi-shift task lands on,
priced by
objective.non_preferred_date_cost (unset/0 = inert).Shipments
A shipment is a bound pickup→delivery pair — a distinctshipments[] entry
type, not a job with a flag. The solver guarantees the pickup precedes the
delivery on the same route; carried demand counts against capacity
only between the two legs.
pickup/delivery are Stop objects — each has its own location,
setup_duration_s, service_duration_s, and time_windows; every other
job field (demand, mandatory, drop_fee, skills, eligible_vehicles,
preferences, tags, preferred_date) is shared and applies to the pair
as a whole (one droppability/eligibility decision, one drop_fee charged
once). target_arrival is not yet supported on shipments (400).
Vehicles & shifts
A vehicle carries capacity, skills, a cost model, and one or more shifts. The shift is where the working window and depot locations live — there is no vehicle-levelcount (fleet size is one entry per physical vehicle).
string
default:"car"
Routing profile for this vehicle’s distance/time matrix:
"car",
"truck", or "bike". Vehicles on different profiles get independently
fetched matrices.object
Named dimensions, matched against job/shipment
demand by name.array
Soft:
[{ "tag": "north", "violation_cost": 25.0 }]. Serving a task that
lacks a preferred tag incurs that entry’s cost; entries apply
independently.object
Hard route caps:
max_distance_m, max_route_duration_s (whole-route
span — travel, service, and wait all count, not pure drive time).
There is no separate pure-drive-time cap.object
Per-vehicle overrides.
fixed (one-time, when used), per_travel_hour,
per_stop — at most one of these three may be set per vehicle
(a second is 400). per_travel_km is a distance-rate override and
combines freely with whichever of the three you picked.number
default:"1.0"
Travel-speed multiplier,
(0, 5]. Effective travel time =
matrix_time / speed_factor — above 1.0 is faster.number
default:"1.0"
Work-rate multiplier for
service_duration_s only, (0, 5] — never
setup_duration_s, depot/reload handling time, waiting, or breaks. A
shift may override it for its own day via shifts[].service_factor.Place
Per-shift depot location.
start is where the vehicle departs, end
where it must return. Absent = open start/end (no depot on that side).integer
Seconds past
to the vehicle may run, priced via
objective.costs.per_overtime_hour (or the vehicle’s own override).integer
Maximum job stops for this shift. Lives on the shift, not
vehicle.limits.array of strings
Hard scope: when set, this shift serves only tasks carrying at least one
of these tags (untagged tasks remain serveable by every shift).
array
Scheduled driver breaks (
floating with a window or a drive/duty
trigger, or fixed). See Constraints.array
Mid-shift depot visits to reset capacity and start a fresh trip
(multi-trip). Capped with
max_trips. See
Constraints.Relations
A singlerelations array expresses every inter-job ordering, grouping, and
synchronisation rule. Each relation is a tagged object with a type, and
every relation is hard — there is no per-relation violation_cost.
Objective
The whole block is optional — omit it for “minimise travel cost, serve everything, use as few vehicles as it takes.”minimize_vehicles— a bool, defaulttrue. Whether using fewer vehicles outranks minimising cost. This is the one real degree of freedom; it replaced anobjective.prioritiestoken list that could never actually be reordered.costs— fleet-wide rates:per_travel_km,per_travel_hour,per_wait_hour,per_overtime_hour,per_late_hour(soft time-window lateness, fleet-wide only),per_vehicle(flat per-route activation charge), plus an informationalcurrencylabel. Omittingcostsentirely seeds a realistic baseline — €0.20/km and €35/hour — and nothing else; every other rate defaults to0/unset.non_preferred_date_cost— flat charge for serving a task on a day other than itspreferred_date.grouping— keeps geographically clustered tasks consecutive in a route (the “passing-waypoint” feature); presence enables it.
drop_fee, preferences[].violation_cost, target_arrival.per_late_hour)
— not in this global block. See Objective Function
for the full two-tier ranking and defaulting behaviour, and
Constraints for every hard/soft rule.
Response
A solve returns asummary, per-stop routes, and unassigned jobs with
structured reasons.
summary.status—solved,partial, orinfeasiblesummary.estimated_cost— always present.components.travel/waitingare always reported;drop_fees/overtimeappear only when priced;lateness/vehicles_fixed/preferences/imbalanceare declared in the schema but never populated today — don’t chart them as zero, they’re absent because they aren’t wired yetroutes[].vehicle— the plainvehicle.idstring, not a{ type, instance }object (there is no vehiclecountto disambiguate — see Migration from V2)routes[].stops[].type—start,job,pickup,delivery,endalways;breakandreloadappear too, on routes whose vehicle has scheduled driver breaks or depot reloads respectivelyroutes[].stops[].load_after— named load map after serving this stopunassigned[].job_id— the id of the unassigned job (a shipment’s bare id, for either leg)unassigned[].reasons— structured{ code, message }objects from a closed machine-code set (TIME_WINDOW_VIOLATED,CAPACITY_EXCEEDED,SKILLS_MISMATCH, …)unassigned[].relaxations— best-effort, actionable suggestions (e.g.{ "op": "extend_time_window", "amount": 900, "unit": "s" }); empty when no simple relaxation exists
Warm-starting with initial_routes
initial_routes seeds the search from an existing plan (re-optimisation)
instead of constructing from scratch:
vehicle, an ordered job_ids list (a shipment id
appears twice — pickup then delivery), an optional shift_index (which of
the vehicle’s shifts this fills, default 0), and locked_count — how many
leading job_ids the search must not move (a dispatched prefix). Unknown
vehicle/job ids are rejected with 400. /v3/routing/evaluate and
/v3/routing/suggest also accept initial_routes for solve-payload
compatibility, but ignore it — routes is their actual input.
Constraints
Every constraint field, hard vs. soft, with examples.
Migration from V2
Field-by-field mapping from the V2 VRP API.
Objective Function
The two-tier lexicographic ranking behind
estimated_cost.API Reference
Full request and response schema for
POST /v3/routing/solve.