V3 is in beta. General availability (GA) is planned for January 2027.
problem key;
a minimal one is problem.jobs and problem.vehicles.
This page describes the request and response of
POST /v3/routing/solve.
/v3/routing/evaluate and
/v3/routing/suggest take the same problem object.
The schema is strict: a field the schema does not define, such as an old
field name or a typo, is rejected with HTTP 400 and an unknown_field item.
Its message names the field and its pointer locates it: the key itself, or
the object that contains it. See Errors.Design principles
- Few orthogonal primitives. Eligibility, skills, and sequencing are each
a single field. A pickup-then-delivery is its own
shipments[]entry type, not a flag on a job. - Everything soft is money. Lateness costs a rate per hour, dropping a task costs a fee, a non-preferred vehicle costs an amount. That is one unit to trade against, with an itemised cost breakdown in every response.
- Penalties live on the thing. A task’s drop price (
drop_cost) and preferences sit on the task object itself: read one job and you see what it costs to bend each rule for it. - Strict schema, named rejections. Unknown fields are
400s with anunknown_fielditem. Fields that are specified but not available yet are400s with anot_supporteditem, rather than being ignored.
A request at a glance
The smallest valid request serves everything and minimises travel cost:problem, plan, and
options. plan is an optional warm start (see
Warm-starting with plan) and options is
covered under Options and travel.
problem holds up to eight fields. vehicles and at least one job or
shipment are required; the rest are optional.
Conventions:
- Durations are seconds (
_s) and distances are metres (_m). - Timestamps are ISO 8601 / RFC 3339 instants. Every timestamp needs a UTC
offset (
+02:00orZ): one without is a400that names the field. Timestamps in a response are in the UTC offset of the shift they belong to: a route’s stop times and a suggestion’s placement times use the offset of that shift’sfrom(Zfor a UTCfrom). - Coordinates are
[longitude, latitude]. A longitude outside-180to180or a latitude outside-90to90is a400without_of_range;[0, 0]is a400withinvalid_value, because it is the usual placeholder for a missing GPS fix. - Ids (
jobs[].id,shipments[].id,vehicles[].id,shifts[].id,depots[].id,quotas[].id) are 1 to 128 characters. - Money is a plain number in your own currency. Every price and rate must be
0or more; the ceilings are listed under Limits.
Jobs
A job is a unit of work at one location, such as a delivery, pickup, service call, or inspection. Its visit fields (location, service_duration_s,
time_windows) sit directly on the object.
string
required
Unique across
jobs and shipments. Echoed as routes[].stops[].id and
unassigned[].task.Place
Where the job is performed: a
[lon, lat] array (GeoJSON order).
Omit location for a
virtual (location-less) job: it still has service duration, time
windows, loads, and skills, but requires no travel, and it can share a
route with located jobs. A shipment leg always needs a location. A job’s
location is always an inline coordinate; only a shift’s start/end can
reference a depot by id. See Vehicles and shifts.integer
Seconds paid once on arrival at a new location, distinct from
service_duration_s and skipped when the previous stop in the route is
already at this location. Not scaled by service_factor. Default 0; a
negative value is a 400.integer
Nominal on-site service time in seconds. Default
0; a negative value is a
400. 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). Omitted: the job can be
served at any time in a shift. Hard by default: a job that cannot be
reached inside any of its windows is left unassigned. A window whose from
is after its to is a 400 with conflicting_fields. Two optional
per-window fields:per_late_hourmakes this window soft on its late side: arriving aftertois allowed and priced at this rate per hour, in place of the fleet-wideobjective.per_late_hour. Must be> 0, and only on a task with exactly one window.choice_costis a flat price for being served in this window rather than one of the task’s other windows. It grades preference among alternatives (the task needs at least two windows) and does not soften any of them.
object
Load that goes onto the vehicle at this visit, as named non-negative
amounts such as
{ "weight": 30, "volume": 2 }. It stays on board until
the next reload or the end of the route, so load_after rises at the stop.
Use it for goods you collect. For goods you drop off, use delivery_load.Dimensions are matched against vehicles[].capacity by name; order never
matters. A vehicle whose capacity map omits a dimension has capacity 0
in it; a vehicle with no capacity at all is not capacity-limited. A
dimension name that no vehicle lists is therefore not rejected: check for
typos if a job comes back unassigned on capacity.object
Jobs only. Load that comes off the vehicle at this visit, having been
loaded at the depot, as named non-negative amounts such as
{ "weight": 10 }. The vehicle leaves each trip carrying the sum of that
trip’s delivery_load quantities, so load_after starts at that sum and falls
at each stop. A job can set both fields: delivery_load for what it drops off
and pickup_load for what it collects.capacity:
On a route that only drops off, or only collects, the peak load is the same
total with either field, and only the direction of
load_after differs. The
choice matters when one route mixes the two: a drop-off frees room for a
later collection only when it is modelled as delivery_load. See
Capacity.
number
What leaving this task unserved costs, in the objective’s currency, and the
only way to make a task droppable: the task is served whenever serving it
is cheaper than the fee. From
0 to 1,000,000,000. Fees paid are reported
in score.components.drop_costs. Cannot be combined with priority
(400).integer
default:"1"
Importance of a must-serve task when not all of them fit. The solver
maximises the summed
priority of served tasks before it considers any
cost, so a higher-priority task is kept ahead of lower-priority ones. Must
be >= 1. It is a rank, not money, and never makes a task droppable. Use
drop_cost for that; setting both is a 400. The summed priority of
must-serve tasks left out is reported as score.unassigned_priority.array
Hard capability requirements:
[{ "name": "fridge" }], each with an
optional min_level (>= 1, default 1). The serving vehicle must provide every
named skill at that level or higher. For a soft affinity use tags with
the vehicle’s preferred_tags, or preferences (below).object
Hard filter:
allowed (whitelist, omit = all vehicles eligible) or
excluded (blacklist), mutually exclusive. An empty allowed list, or a
vehicle id that matches no vehicle, is a 400. To pin the job to one vehicle,
list only that vehicle in allowed.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. Each
violation_cost is 0 or more; a vehicle listed twice, or an unknown
vehicle id, is a 400. Reported in score.components.preferences.array
Absolute price of serving the task on a vehicle:
[{ "vehicle": "v2", "cost": 12.5 }]. The vehicle that serves the task
pays its cost once (any of its shifts); unlisted vehicles cost 0.
Unlike preferences, which charges the vehicles that did not get the
task, this prices the one that did, so a task rated badly everywhere is
dropped first when capacity runs out. Below priority, and additive with
preferences, drop_cost and travel. Each
cost is 0 or more; a vehicle listed twice, or an unknown vehicle id, is
a 400. Reported in score.components.assignment.array of strings
Free-form labels used for relation
tag matching and, when a shift
scopes itself via serves_tags (hard) or a vehicle prices preferred_tags
(soft), for tag matching. A task matches when it carries any one of the
listed tags, unlike skills, where all must be provided.object
Soft point deadline:
{ "at": "...", "on": "arrival", "per_late_hour": 60.0 }.
on is arrival (default) or departure (the end of the visit), named
after the stop fields; any other value is a 400. at and per_late_hour are required, and per_late_hour must be
> 0. Reaching that instant after at is priced, never infeasible, and an
early arrival never waits for it. Independent of
objective.per_late_hour. On a shipment, measured on the delivery leg.array
Per-vehicle work-rate factors for this task:
[{ "vehicle": "v1", "factor": 1.25 }]. Service time is divided by the
factor (above 1 = faster), and it multiplies the serving vehicle’s own
rate (the shift’s service_factor if set, else the vehicle’s): service time
= service_duration_s / (shift or vehicle factor × factor). Each factor
is in [0.001, 5]; an unknown vehicle id, or a vehicle listed twice, is a
400. A vehicle not listed works at its own rate. V2’s durationModifier
multiplied the duration instead: when migrating, send
1 / durationModifier here.Shipments
A shipment is a bound pickup→delivery pair with its ownshipments[] entry
type. The solver guarantees the pickup precedes the delivery on the same
route; carried load counts against capacity only between the two legs.
pickup and delivery each have their own required location, plus
setup_duration_s, service_duration_s, time_windows, and an optional
stop id (default "{shipment}:pickup" / "{shipment}:delivery"). A stop
id, set or defaulted, must not collide with any job, shipment or other stop
id (400). The shipment’s load (named non-negative amounts, like a job’s
pickup_load) replaces the job load fields. Every other job field (drop_cost,
priority, required_skills, eligible_vehicles, preferences,
assignment_costs, tags, target, service_factors) is
shared and applies to the pair as a whole: one droppability and eligibility
decision, one drop_cost charged once.
On a shipment, delivery is the delivery stop object shown above. It is not
the jobs[].delivery_load load field, which exists on jobs only.
Shipments take part in relations in a limited way. See
Relations and shipments.
Vehicles and shifts
A vehicle carries capacity, skills, a cost model, and one or more shifts. The shift is where the working window and start/end locations live. The fleet is onevehicles[] entry per physical vehicle.
string
required
Unique across
vehicles[]. Echoed as routes[].vehicle.string
default:"car"
Routing profile for this vehicle’s travel times and distances:
"car",
"truck", or "bike". Any other value is a 400.object
Named dimensions, matched by name against job
pickup_load and delivery_load and
shipment load, as non-negative integers. Omitted: no load limit in any
dimension. A request may use at most four distinct dimension names. See
Capacity.array
Capabilities the vehicle provides:
[{ "name": "fridge", "level": 2 }]
(level optional, default 1). Always objects: a bare string is
rejected. A shift can replace the list for its own day with
shifts[].skills.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 caps on every shift of the vehicle:
max_stops stops per route (a
job counts as one stop, a shipment as two; start, end, break and reload
stops do not count), max_distance_m and
max_route_duration_s per route
(whole-route span: travel, service, and waiting all count, the same
quantity as routes[].time.span_s), max_leg_travel_s for any
single travel leg (vehicle only, at least 1), and periods[] for caps across
all of the vehicle’s shifts in a day, a week, or an explicit window. See
Constraints.object
This vehicle’s own rates: the same fields as
objective.costs
(per_route, per_used_vehicle, per_route_hour, per_travel_km,
per_wait_hour, per_overtime_hour). Each field set here replaces the fleet value for
this vehicle; a field not set keeps the fleet value. Nothing is added on
top, and each field is reported under the same score.components key as
the fleet rate: a vehicle with "per_route_hour": 30 costs 30 per hour of
route span, booked in components.time, and still pays the fleet per_route.
Any fields combine on one vehicle.number
default:"1.0"
Travel-speed multiplier,
[0.001, 5]. Effective travel time =
travel_time / speed_factor, so above 1.0 is faster.number
default:"1.0"
Work-rate factor for
service_duration_s only, [0.001, 5]. Service time
is divided by it, so above 1 is faster: effective service time =
service_duration_s / service_factor. Never applied to setup_duration_s,
depot or reload handling time, waiting, or breaks. A shift’s own
shifts[].service_factor replaces it for that day, and a task’s
service_factors[].factor for this vehicle multiplies whichever applies.array of strings
Vehicle ids this vehicle may share the tasks of a
compatible_vehicles
relation with. Omitted: any vehicle. A list is a veto: two vehicles may
share a relation only if neither’s list excludes the other. A vehicle
always pairs with itself, so list only its own id to pair it with no other.
An empty list, or an unknown vehicle id, is a 400. Has no effect without
a compatible_vehicles relation.string
required
1 to 128 characters, unique within the vehicle. Echoed as
routes[].shift.string (ISO 8601 instant)
required
Start of the working window.
string (ISO 8601 instant)
required
End of the working window. The route, including any unload at
end, must
finish by to, or by to + max_overtime_s. A to before from is a
400 with conflicting_fields.object
Where the shift starts: exactly one of
location (an inline place) or
depot (a depots[].id); both, or neither, is a 400. An optional
service_duration_s (seconds, >= 0) overrides the handling time there;
it defaults to the depot’s service_duration_s, or 0 for a location,
and is not applied on an ignored leg. A shift whose start and end handling
alone do not fit between from and to + max_overtime_s is a 400. Omit start for a
route that is open on its start side.object
Where the shift ends, in the same shape as
start. Omit end for a route
that is open on its end side.boolean
default:"false"
When
true, the leg from start to the first task costs nothing and takes
no time, while the start stop is still reported. Use it when the
vehicle’s commute should not count. Requires start: true without one is
a 400 conflicting_fields.boolean
default:"false"
The same for the leg from the last task to
end; requires end.integer
Seconds past
to the vehicle may run, >= 0. Omitted: to is a hard
end. Overtime is priced at the shift’s costs.per_overtime_hour, else the
vehicle’s, else objective.costs.per_overtime_hour; while none is set,
overtime is allowed at no cost. A synchronized or time_lag hold never
runs into overtime: it stops at to.object
This shift’s capacity, per named dimension. A dimension it names replaces
the vehicle’s
capacity there; the others keep the vehicle’s value
(unlimited when the vehicle sets no capacity).object
This shift’s
max_stops, max_distance_m and max_route_duration_s.
Each one set replaces the vehicle’s limits value for this shift; the
others keep the vehicle’s. periods[] stays on the vehicle.object
This shift’s own rates, the same fields minus
per_used_vehicle. Each field set here
replaces the vehicle’s value (or, when the vehicle does not set it, the
fleet’s) for this shift only.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). Omitted or
empty: the shift serves everything.
array
This shift’s skills, in the same
{ "name", "level" } shape as
vehicles[].skills. Replaces the vehicle’s list for this shift only;
[] means the shift provides none. Omitted: the vehicle’s skills.number
This shift’s work rate, replacing
vehicles[].service_factor for the
shift, in the same [0.001, 5] range and direction (service time is divided
by it). Omitted: the vehicle’s value. A task’s service_factors[].factor
multiplies it.array
Scheduled driver breaks (
windowed, triggered by drive or duty time, or
fixed). They appear in the response as break stops. See
Constraints.array of strings
Depot ids (from
depots[]) the vehicle may return to mid-shift to empty
its load and start a fresh trip. Each visit takes that depot’s
service_duration_s and is charged its per_reload, and appears in
the response as a reload stop. Cap the number of trips with max_trips.
An unknown depot id, or one listed twice, is a 400. See
Constraints.integer
The most trips the shift may run, across all of its reload depots: trips
= reloads used + 1, so
1 allows no reload. At least 1; 0 is a 400.
Omitted: trips are not capped, though one reload depot is still visited
at most 8 times per shift.object
Load already on the vehicle at shift start, per named dimension, as
non-negative integers. It occupies capacity on the first trip only and is
cleared at the first reload. It is not checked against
capacity: a value
at or above capacity is accepted and shows up as tasks left unassigned, not
as a 400.Depots
problem.depots[] lists the places a shift can start from, end at, or
reload at, referenced by id from shifts[].start, shifts[].end, and
shifts[].reloads:
id (1 to 128 characters, unique across depots[]), a
location, an optional service_duration_s and an optional
per_reload:
service_duration_sis the handling time per visit in seconds: the load before a shift that starts there, the unload after a shift that ends there, and each reload. Default0; a negative value is a400. A shift’sstart.service_duration_sorend.service_duration_soverrides it for that end.service_factornever scales it.per_reloadis charged once per reload visit, not for starting or ending a shift there, and reported inscore.components.reloads. Default0, at most 1,000,000,000.
Relations
A singlerelations array expresses every ordering, grouping, and timing
rule between tasks. Each relation is a tagged object with a type, and every
relation is hard.
same_vehicle, same_day, same_route, time_lag and compatible_vehicles
constrain the members that end up
assigned: an unassigned member imposes nothing on the others. On
evaluate, a plan that breaks a relation reports a
violation whose code is the relation’s type; the first-stop pin of a
single-entry sequence relation, and the order of a consecutive one, report
as sequence. A stop between consecutive members is not reported yet.
These ten are the complete set. Any other
type value is a 400 with an
invalid_type item. See Constraints
for the selection rules and the combinations that are rejected.Relations and shipments
Relations act on jobs and on shipment legs. A leg is named by its stop id (pickup.id / delivery.id, by default "{shipment}:pickup" /
"{shipment}:delivery") and is then related like a job by sequence,
same_vehicle, same_day and same_route. A leg coupled to other tasks by
synchronized or time_lag, directly or through a sequence, is a 400.
What happens when a relation names the shipment itself depends on the type:
No relation constrains a whole shipment; naming one is rejected instead of
being dropped after a
200. To order or group a shipment, name its legs in
stops; a tag can only select the whole shipment. A shipment’s own
pickup-before-delivery order on one route always holds.Objective
The whole block is optional. Omit it and the solver serves every task it can, then minimises travel cost at the default rates.-
costs: the fleet rates.vehicles[].coststakes the same fields andshifts[].coststhe same minusper_used_vehicle, and each field resolves on its own: the shift’s value, else the vehicle’s, else the fleet’s, else the default.per_travel_kmis charged per kilometre driven. Default 0.20.per_route_houris charged per hour of each route’s span from start to finish (travel, service, waiting, and breaks;routes[].time.span_s). Default 35.per_wait_hourandper_overtime_hourare extra rates for waiting time and for time past a shift’sto. Default0.per_routeis a flat charge for each route that serves at least one task (a vehicle has one route per shift). Default0.per_used_vehicleis a flat charge once for each vehicle that serves at least one task on any shift, however many shifts it works. It adds toper_route, exists onobjective.costsandvehicles[].costsbut not onshifts[].costs, and defaults to0.
0prices that term at zero. Every value must be0or more. Rates are rounded to 1/100,000 of a currency unit per metre or per second, and a non-zero rate that rounds to zero is a400rather than silently becoming free. -
currency: an informational label echoed asscore.currency. It does not convert anything. -
per_late_hour: prices lateness on every time window that does not carry its owntime_windows[].per_late_hour, making those windows soft on the late side. Omit it to keep windows hard; an explicit0is a400. -
per_delay_workday: ASAP scheduling. Money charged for each served task per workday its shift lies after the earliest workday in the request. A workday is a distinct date, in the shift’s own UTC offset, on which at least one shift starts; shifts on one date share a rank. Unserved tasks pay nothing; the charge never outrankspriority, and it cannot be combined withdrop_cost(a400). Omit it or send0to switch it off. When every shift starts on one date it charges nothing, but thedrop_costrejection still applies. Reported inscore.components.delay. -
grouping: keeps tasks that lie withinradius_mof each other consecutive in a route by chargingper_entry(required,0to1000) on every entry into such a group from a job outside it, the first entry included; entering from the shift’s start or a reload is free. Present = enabled.radius_mdefaults to180and must be> 0.distance_metricselects how the radius is measured:straight_line(default) orroad. -
workload_share: spreads service time across vehicles. Each vehicle’s scheduled service time (travel, setup, waiting and breaks are not counted) is compared with its share of all the work, and the part outside a band oftolerance(default0.1) is chargedper_imbalance_hour. Present = enabled;vehicles[].workload_weightscales a vehicle’s share (0leaves it out). Reported asscore.components.workload_share. -
Vehicle count: there is no separate flag for it. The number of
vehicles used is whatever minimises total cost. To consolidate work onto
fewer vehicles, put a price on using one:
costs.per_routefor each used shift, orcosts.per_used_vehicleonce per vehicle however many shifts it works, on the whole fleet (objective.costs) or per vehicle.
drop_cost, preferences[].violation_cost, target.per_late_hour,
time_windows[].per_late_hour), not in this block. See
Objective function for how plans
are ranked, and Constraints for every hard and
soft rule.
Options and travel
options sits next to problem at the top level of a solve request:
number
default:"150 ms plus 4 ms per task"
Search time budget in seconds; fractions are allowed (
2.5). Omitted, it is
150 ms plus 4 ms per task (tasks are jobs and shipments together). Maximum
60; a larger or negative value is a 400. A value below 0.1 is raised
to a 100 ms minimum. This is search time only: the
travel-matrix fetch and any wait for capacity come on top.integer
default:"42"
Random seed for the search. Repeated runs with the same seed on the same
hardware usually return the same plan. Omitting
seed is the same as
sending 42, so to explore other plans you have to send different values.string
simplified or full. When set, each route carries its road geometry as
routes[].polyline (an encoded polyline, precision 5). Omitted = no
geometry.string
default:"estimate"
What a solve does when travel times cannot be fetched from the travel data
provider, whatever the reason (outage, timeout, overload, an error from the
provider).
estimate solves on travel estimated from
straight-line distance and a typical speed for the vehicle profile, and
sets summary.travel_estimated to true. fail returns the 502 or
503 instead. Coordinates the provider rejects for spanning more than one
region are a 400. Evaluate and suggest have no such option and always
estimate. See Distance matrices./v3/routing/suggest has its own options: max_insertions (how many
ranked insertions to return per task, 5 by default and at most 10) and time_limit_s (the search budget,
default 1 second). The tasks to place come from problem, not from
options. See Suggest.
/v3/routing/evaluate takes no options. See
Limits for request size and count caps, and the
Performance guide for choosing a
time budget.
problem.travel controls where travel times and distances come from:
string
tomtom uses typical traffic for the departure time. osm uses free-flow
times with no traffic. straight_line uses great-circle distance at
travel.speed_kmh, with no road network. Omitted = osm, or tomtom
when departure_time is set. tomtom_real_time is not enabled and
returns 403 feature_not_enabled.number
default:"50"
The speed for
engine: straight_line, from 1 to 1000 km/h. Rejected
with 400 alongside any other engine.string (ISO 8601 instant)
The instant the traffic lookup is made for, such as tomorrow’s 08:00 to
plan with morning-rush travel times. Omitted = free-flow times, unless
engine: tomtom asks for traffic at the time of the request. Rejected
with 400 alongside engine: osm or engine: straight_line. A request that
sets it skips the server-side travel-matrix cache, so it fetches fresh
travel data every time.Response
A solve returns asummary, per-stop routes, the same routes as a plan,
and unassigned tasks with structured reasons. The example below serves two
jobs on one route with a driver break and a mid-shift reload, and leaves a
third job unassigned. It is a standalone illustration, priced at the default
rates (35 per hour, 0.20 per km) with no per-vehicle or depot cost:
Summary
object
{served, unassigned, unassigned_required}. A job is one task and a shipment is
one task. unassigned is the length of unassigned[]; unassigned_required
counts the unassigned tasks with no drop_cost. A plan that leaves tasks out
is still HTTP 200: check tasks rather than the status code.object
{termination, iterations}: how the search ended. termination is
time_limit (the search used up its budget: options.time_limit_s, or
the lower cap a one-vehicle solve with time windows gets),
no_improvement (it stopped early because the best solution stopped
improving; not a proof of optimality; warm-started and some one-vehicle
solves can end this way) or not_searched (/evaluate, with
iterations 0). See Performance.
New values may be added: accept unknown ones.integer
The number of distinct vehicles with at least one route.
integer
The number of entries in
routes[]. A route is one vehicle on one shift,
so a vehicle that works two shifts counts twice here and once in
vehicles_used.integer
The sum of
routes[].distance_m, in metres.object
The sum of
routes[].time, in seconds: span_s, travel_s, service_s,
depot_s, wait_s, break_s and overtime_s. span_s is the wall-clock
span the time cost component is charged on; travel_s is driving time
only. wait_s is the sum of the stops’ wait_s, the idle before a
windowed break’s window opens included, so it can exceed the waiting
cost component and what per_wait_hour charges for. The parts sum to
span_s with two exceptions: setup time is inside the span with no field
of its own, and a fixed break that begins while the vehicle is driving
overlaps that drive, so the overlap counts in both travel_s and
break_s.object
violation_count, unassigned_priority, cost, currency, and components.
violation_count is always 0 on a solve. unassigned_priority is the summed
priority of must-serve tasks left unassigned (a task that priced a
drop_cost counts in components.drop_costs instead). Every component is
always present and the components sum to cost. currency appears only
when you sent objective.currency. See
Objective function.object
total_ms is the wall-clock time the request took, in milliseconds;
search_ms is the part spent in the search (absent on evaluate).boolean
true when travel times could not be fetched from the travel data
provider and the plan was built on estimated travel (straight-line
distance with a detour factor, at a typical speed for the profile, with no
one-way streets or traffic). The plan, unassigned[].reasons and score.violation_count hold
only against that estimate, and no routes[].polyline is returned.
Absent when travel came from the road network. On a solve, set
options.travel_fallback to fail to get an error instead; evaluate
always estimates.summary of an evaluate response has the same
shape.
Routes
routes[] has one entry per route that serves at least one task. A shift
with no work has no entry.
string
The vehicle id from the request.
string
The id of the shift this route runs on.
integer
Total distance driven on the route, in metres.
integer
Driving time only, in seconds: the sum of the stops’
travel_s.integer
The route’s span in seconds, from the
start stop’s arrival to the
end stop’s departure. It includes service, waiting, and breaks, and is
what per_route_hour is charged on.integer
Seconds the route runs past the shift’s
to; 0 when it finishes within
the shift.object
The highest load reached on the route, as a named map. Absent when the
request names no load dimension.
array
The stops in time order (by
departure), including the start and end
stops. A windowed break taken while the vehicle is at a stop is listed just
before that stop and lies within that stop’s arrival to departure, the
end stop included. Drive, duty and unavailability breaks end before
the next stop’s arrival. See Stops.string
The route’s road geometry as an encoded polyline (precision 5). Present
only when you set
options.polyline, and absent for a route whose
geometry could not be produced. distance_m and time come from
the travel data the plan was built on, so rely on them if they differ from
the geometry.Stops
string
One of
start, job, pickup, delivery, break, reload, end.
Every route has one start and one end. pickup and delivery are the
two legs of a shipment.string
On a
job stop, the job id. On a pickup or delivery stop, that leg’s
own id, or "{shipment}:pickup" / "{shipment}:delivery" when it has
none. On a break stop, the id of the request break
(shifts[].breaks[].id) it realises, absent when that break named none.string
The
depots[].id of the stop: on every reload stop, and on a start or
end stop when the shift names a depot there.array
The stop’s coordinate as
[longitude, latitude]. Absent on break stops
and on the start or end stop of a route that is open on that side. A
virtual job reports the coordinate of its nearest located neighbour on the
route: the previous located stop, else the next one. It is absent only on
a route with no located stop at all.array
The same point snapped to the road network, when available.
string (ISO 8601 instant)
Arrival at the stop, before any waiting. Service starts at
service_start. On a break stop, when the break starts. Always present
on every stop. In the UTC offset of the route’s shift from: a shift
starting 2026-07-01T08:00:00+02:00 reports 2026-07-01T08:10:00+02:00.
A shift from in UTC reports Z.string (ISO 8601 instant)
When service begins, on task and reload stops:
arrival + wait_s plus the
duration of any windowed break taken on arrival there. In the same offset
as arrival.string (ISO 8601 instant)
Departure from the stop, in the same offset as
arrival.integer
Seconds spent waiting before service starts: for the time window to open,
for any hold reported in
hold_s, and for a windowed break’s window to
open when the break is taken on arrival there. On an end stop, the idle
time at the end, never negative: departure - arrival - service_s less
the service_s of break stops taken there. Idle before a break’s window
opens is charged through the span, not by per_wait_hour.integer
The part of
wait_s spent holding for a synchronized or time_lag
partner on another vehicle. Present only when it is above 0.integer
Time spent at the stop, in seconds. On a task stop this is the effective
service time, after the vehicle’s
service_factor, so it can differ from
the service_duration_s you sent; setup_duration_s is not included. On
a start, end, or reload stop it is the depot handling time. On a
break stop it is the length of the break, so
departure - arrival == service_s. service_factor never scales depot,
reload or break time.The timeline reconciles per stop. On a task or reload stop,
departure - service_start - service_s - break_s is the stop’s setup time (break_s is absent, so counts as 0, on a stop without an interrupting break): the
request’s setup_duration_s for the first stop of a run at the same location (setup is charged when the previous stop is elsewhere), and 0 for
a co-located successor. service_start is arrival + wait_s plus the
duration of any windowed break taken there. On a start or end stop,
departure - arrival - wait_s - Σ service_s of the windowed break stops
taken there == service_s (a start has no wait).integer
Seconds of driver break taken in the middle of this stop’s service,
pausing it. Only a job with
break_interruptible: true can be paused:
a fixed, windowed or triggered break that falls inside its service
gets no break stop of its own and is counted here and in the route’s
time.break_s. Present on job stops only, when above 0.integer
Driving time from the previous stop, in seconds.
integer
Distance from the previous stop, in metres.
integer
Seconds the start of service, measured from the arrival plus any
windowed
break taken on it (idle until its window opens included), is past the task’s
time window; 0 when on time. A target is reported separately, in
target_lateness_s.integer
Seconds past the task’s
target.at, measured at arrival (plus any
windowed break taken there, idle before it included) or departure as
target.on says; 0 when met. Present only on a stop whose task has a
target. For a shipment it is on the delivery stop.object
The load on the vehicle after this stop, as a named map. Absent when the
request names no load dimension.
type, arrival and departure are
on every stop, and the schema marks only those three as required; the other
fields appear on the stop types below and are absent elsewhere:
routes[].violations and routes[].stops[].violations appear only in an
evaluate response. A solve response never has them.
Plan
object
The same routes in the request’s
plan vocabulary: plan.routes[], each
with vehicle, shift, and stops[] of type job, pickup, delivery,
or reload. Feed it straight into evaluate, suggest, or a warm-started
solve. See Warm-starting with plan.Unassigned tasks
unassigned[] has one entry per job or shipment that is not on a route. It
is an empty array when everything is assigned.
string
The id of the unassigned job, or the shipment id for a shipment.
string
Always present, one of:
impossible (no vehicle can ever serve it),
blocked (a vehicle could serve it alone, but not alongside the tasks the
plan serves), priced_out (paying its drop_cost was cheaper than serving
it), or time_limit (a worthwhile placement exists but was not found
within the time budget, the one case a longer time_limit_s can help).array
{ constraint, message } objects, one per rule that refused the
task, named in constraint from the same vocabulary as evaluate’s
violation code (time_window, capacity, required_skills, …).
priced_out and time_limit carry one reason without a constraint.
New values may be added, so accept unknown ones. message is for people.
See Reasons.array
Best-effort suggestions for a change that would let the task be served.
Empty when no simple relaxation exists. Each entry has:
op:extend_time_windoworreduce_load.amountandunit: the size of the change.unitissfor seconds onextend_time_windowandloadonreduce_load.pointer: a JSON Pointer to the request field to change:/problem/jobs/{i}/time_windowsor/problem/jobs/{i}/pickup_load(/delivery_loadwhen the job only delivers) for a job,/problem/shipments/{i}/pickup/time_windowsor/problem/shipments/{i}/loadfor a shipment.message: the same advice as a sentence for people.
Search progress and warnings
Both are top-level fields, not undersummary.
array
How the best plan improved during the search, oldest first, as
{elapsed_ms, unassigned_priority, cost} entries. The first is the plan
the search started from, each one after it is a better plan, and the
last is the final plan: it equals summary.score, and its elapsed_ms
equals summary.timings.search_ms. It can equal the entry before it, or
be slightly worse. elapsed_ms counts from the start of the search;
unassigned_priority and cost mean what they mean in summary.score.
At most 500 entries, a longer curve thinned evenly; some large solves
without a plan have the final entry only. Present on every solve,
absent on evaluate. See
Performance for using it to
choose time_limit_s.array
Inputs your request carried that changed nothing, as
{code, pointer, message} items. Always present, [] when there are none; /suggest
returns it too. At most 100 are returned. No input produces one today;
new codes may be added, so accept unknown ones.Pointers
errors[].pointer (on a 400) and unassigned[].relaxations[].pointer (on
a 200) are both JSON Pointers from the root of the body you sent, so they
start with /problem, /plan or /options and name a task by its index in
the array: /problem/jobs/1/priority, /problem/shipments/0/load. See
Errors for the pointer rules.
Warm-starting with plan
The request-level plan seeds the search from an existing plan
(re-optimisation) instead of constructing from scratch:
vehicle and a shift id (the vehicle’s shift this plan
fills), an ordered stops list, and an optional locked_count: how many
leading stops the search must not move (a dispatched prefix; cannot be
combined with a reload stop on the route, and cannot exceed the route’s
number of stops). A task stop has a type of
job, pickup, or delivery and an id, as in
{ "type": "job", "id": "job-1" }. A shipment leg’s id is its own id, or
"{shipment}:pickup" / "{shipment}:delivery" when it has none. A reload
stop is { "type": "reload", "depot": "..." }, naming a depot from that
shift’s reloads and splitting the route into a new trip. Start, end, and breaks are
placed by the solver and are never listed. plan is the same shape a
solve response returns, so result.plan goes back in unchanged.
A plan that names an unknown vehicle, shift, depot, or stop id, lists a stop
twice, or is otherwise malformed is rejected with 400. The item’s
pointer is /plan/routes/{i} or, for a stop-level problem,
/plan/routes/{i}/stops/{k}.
plan is a hint, not a guarantee on solve:
- An unlocked stop that violates a hard constraint (capacity, skills,
eligibility, a limit, or a hard time window) is taken out of its position.
The search reinserts it wherever it fits, or it appears in
unassigned[]with a reason. - A locked stop (inside
locked_count) that violates a hard constraint can be neither moved nor dropped, so the request fails:400, item codeinvalid_value, pointer/plan, with a message naming the stop, the vehicle, and the rule it breaks. - Time windows are hard by default, so a stop the plan serves after its
window closes is treated as violating. Price lateness
(
time_windows[].per_late_hourorobjective.per_late_hour) and the stop is kept.
POST /v3/routing/validate/* does not cover
them.
To score a plan as it stands, without searching for a better one, send it
to POST /v3/routing/evaluate. It fetches the travel matrix like a solve
does, then returns the plan’s timing, loads, and score, with each broken
rule listed as a violations[] entry on the stop or route instead of
dropping the stop. See Evaluate.
Errors
The three endpoints report errors in one JSON body. A401 and a 429 are
the exceptions: they are answered before the request reaches the routing
service and can have a different body or none, so branch on the HTTP status
for those two. See Errors. A request that does not
parse or does not validate is HTTP 400:
errors[]has one item per problem found.errors[].codesays what kind of problem it is (unknown_field,missing_field,invalid_type,invalid_value,not_supported,out_of_range,limit_exceeded, …).errors[].pointer, when present, is a JSON Pointer into the request body you sent, such as/problem/depots/0/service_duration_s,/plan/routes/0/stops/2, or/options/time_limit_s. It usually ends at the offending key and sometimes at the object that contains it; the message always names the field.relaxations[].pointeruses the same notation: see Pointers.- With one item, the top-level
messagerepeats that item’s message; with several it reads"{n} problems in request". - Branch on
codeanderrors[].code; the messages are for people.
See Errors for every item code, retry guidance, and
authentication failures.
Constraints
Every constraint field, hard vs. soft, with examples.
Evaluate
Score an existing plan and list the rules it breaks.
Suggest
Rank insertion positions for new tasks in an existing plan.
Objective function
How plans are ranked and how
score is built.Solution quality
How to judge a plan from the response fields.
Distance matrices
Where travel times and distances come from.
Migration from V2
Field-by-field mapping from the V2 VRP API.
API reference
Full request and response schema for every endpoint.