V3 is in beta. General availability (GA) is planned for January 2027.
unassigned[]. A soft rule is priced into summary.score.cost
and traded off against travel.
For how the pieces of a request fit together, see
The V3 API model. For how soft costs are ranked, see
Objective function.
A field the schema does not define is rejected with HTTP
400 and
errors[].code: "unknown_field". A field the schema defines but the solver
does not honour yet is rejected with HTTP 400 and
errors[].code: "not_supported"; these are marked Not supported yet below.
Each error item names the offending value with a JSON pointer, for example
/problem/vehicles/0/shifts/0/breaks/1. See Errors.Overview
Capacity
A vehicle declares acapacity per named dimension; a job declares a
pickup_load, a delivery_load or both, and a shipment declares a load. The load
never exceeds capacity in any dimension at any point along the route.
weight, volume, pallets, and so on).
Values are non-negative integers; a negative value is a 400. A request may
use at most four distinct dimension names (see Limits).
Check dimension names for typos, because a mismatch is not an error:
- A vehicle whose
capacityomits a dimension has capacity0in that dimension, so a job demanding it cannot ride that vehicle. - An empty object,
"capacity": {}, omits every dimension: the vehicle carries nothing that has apickup_loador adelivery_load. - A vehicle with no
capacityfield at all has no load limit in any dimension. To mean “no limit”, omit the field; do not send{}. - A shift’s own
capacityreplaces the vehicle’s in each dimension it names; the other dimensions keep the vehicle’s value, or no limit when the vehicle sets nocapacity.
unassigned[] with
capacity.
The response reports load_after on each stop and load_peak on each route,
as the same named map.
Deliveries from the depot
jobs[].delivery_load is a quantity that leaves the vehicle at the visit, having
been loaded at the depot. The vehicle starts its trip carrying the sum of the
trip’s delivery_load quantities. Set pickup_load on the same job for what it
collects there, so one visit can both drop off and pick up:
Loading and unloading different amounts at one address
A shipment carries one quantity from its pickup to its delivery. A swap that collects one quantity and drops off another at the same address is one job with bothpickup_load (picked up) and delivery_load (dropped off), as above.
Time windows
A job or shipment leg carries one or moretime_windows. This is a jobs[]
entry with one window:
time_window. Several windows mean “any of these is acceptable”. A
vehicle that arrives before from waits. The start is measured from the
arrival plus any windowed break taken on arrival at
the stop, including the idle until that break’s window opens, so a break that
pushes the start past to misses the window. A task without time_windows
can be served at any time in a shift. A window
whose from is after its to is a 400 with conflicting_fields.
Write every timestamp with a UTC offset or Z. A timestamp without one is a
400 that names the field.
Four fields price timing instead of forbidding it:
number
A flat cost for serving the task in this window rather than another of
its own windows. Every window stays hard. Needs at least two windows on the
task, and once any of them has a non-zero cost the task’s windows must not
overlap. A non-zero
choice_cost anywhere in the request cannot be combined with
objective.per_late_hour (400). From 0 to 21474.83647. Reported in
score.components.window_choice.number
Fleet-wide: starting service after
to becomes allowed and priced per hour
late, on every window of every task that does not set its own rate. It
applies to task windows only; the shift end (plus max_overtime_s) stays a
hard limit.
Omit the field for hard windows: an explicit 0.0 is a 400. At least
0.0006 and at most 1288490. Reported in score.components.lateness and per stop as
lateness_s.number
Per window: makes this window’s late side allowed and priced at this rate,
replacing the fleet rate for the task and working whether or not a fleet
rate is set. Hard and soft windows can therefore share one request. Must be
at least
0.0006 and at most 1288490, and the task must have exactly one window.
Early arrival still waits. Lateness is measured from the same instant as a
hard window: the arrival plus any windowed break taken on arrival.object
A soft deadline on one task, independent of its windows:
{ "at": "2026-04-01T10:00:00Z", "on": "arrival", "per_late_hour": 60.0 }.
on is arrival (default) or departure (the end of the visit).
Reaching that instant after at is priced per hour and never makes the task
infeasible; arriving early never forces a wait. per_late_hour must be
> 0 and at most 21474.83647. On a shipment, target is measured on the
delivery leg. A leg’s own pickup.target or delivery.target sets a target
on that leg; target with delivery.target is a 400. Reported per stop as
target_lateness_s and in score.components.target.400, so a soft rule cannot be made “almost
hard” with a very large number. For a deadline that must hold, use a hard
window.
Shift hours and overtime
A vehicle works only inside a shift’sfrom and to. The whole route,
including the return leg and any unloading at the end depot, must finish by
to unless max_overtime_s grants an extension. This is a shifts[] entry:
id of 1 to 128 characters, unique within its vehicle.
It is echoed as routes[].shift in the response. Several shifts[] on one
vehicle model availability over several days.
A shift whose to is before its from is a 400 with conflicting_fields.
With max_overtime_s set (>= 0), to becomes a soft end and
to + max_overtime_s the hard one. Time worked past to is reported as
routes[].time.overtime_s and priced at per_overtime_hour, resolved per
route: the shift’s costs, else the vehicle’s, else objective.costs.
score.components.overtime is 0.0 unless one of them sets the rate.
A synchronized or time_lag hold never runs into overtime: it stops at
to.
boolean
default:"false"
The leg from
start to the first task costs nothing: it is left out of the
objective, the schedule and the distance and duration totals. The start stop
is still reported. 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.boolean
default:"false"
The shift starts at its first task: the leg from
start takes no time, so
the first arrival is from plus any start handling. The leg’s
distance is still charged at per_travel_km and reported, so vehicles that start
closer to their work stay cheaper. Its time is not charged and does not
count toward max_route_duration_s, break triggers or max_leg_travel_s.
Requires start, and cannot be combined with free_start_travel: either
is a 400 conflicting_fields.How days are counted
Three rules on this page refer to a day: thesame_day relation, recurring
period limits and the objective.per_delay_workday ASAP term. Each one assigns a whole shift
to a day by the shift’s from. None of them looks at the time of an
individual stop, so a stop served after midnight belongs to the day its shift
started on. A quotas[] window is different: it counts each stop
by its own service start.
All three read the calendar the same way: a day is the calendar date of a
value in the UTC offset it is written with.
The offset a
from is written with decides its day, so write it in the
offset of the place the shift runs in. These two shifts[] entries are both
on 1 April at +10:00:
early starts at 2026-03-31T22:00:00Z and late at 2026-04-01T03:00:00Z,
but both are written with the date 1 April at +10:00, so:
same_daytreats them as the same day. Two members of onesame_dayrelation can be served one onearlyand one onlate.- A period with
every: "day"puts both shifts in 1 April and applies one cap to the two together.
early as "from": "2026-03-31T22:00:00Z" keeps the same instant but
moves the shift to 31 March for both rules: same_day no longer pairs it
with late, and a daily period counts it on 31 March.
There is no soft per-task day preference: preferred_date and
objective.non_preferred_date_cost are a 400 (unknown_field). To keep a
task on one day, limit its time_windows to that day. To pull work onto earlier days instead,
set objective.per_delay_workday.
Skills
A job lists the skills it needs; the vehicle that serves it must hold every one of them.ann can serve boiler-7; bob cannot.
- Skills are objects on both sides. A bare string such as
"gas"is a400witherrors[].code: "invalid_type". - A job requirement may add
min_level(>= 1, default1). A vehicle entry may addlevel(>= 1, default1). The vehicle qualifies when its level is at least the job’smin_level. - A skill name listed twice in one vehicle’s or one shift’s
skillsis a400: give one level per skill. - A vehicle that omits
skillscan serve only jobs that require none. - A shift’s own
skillsreplace the vehicle’s for that shift;[]means the shift provides none. - Skills are always hard. A job whose requirement no vehicle meets is returned
in
unassigned[]withrequired_skills.
Tags, serves_tags, and preferred_tags
Tags describe zones and territories rather than capabilities. Where skills
require every listed entry, tags match on any overlap.
shifts[].serves_tags is hard: when it is set and non-empty, the shift serves
only tasks that carry at least one of the listed tags. Untagged tasks can be
served by every shift. A tagged task that no shift serves is returned in
unassigned[] with serves_tags.
vehicles[].preferred_tags is soft: serving a task that lacks a preferred tag
costs that entry’s violation_cost, which is 0 or from 0.00001 to 21474.83647. Entries apply
independently, so a task lacking several preferred tags is charged for each,
and one vehicle’s entries must sum to at most 21474.83647.
The total is reported in score.components.tag_preference.
Vehicle eligibility
Two fields decide which vehicle may serve a task, and a third prices the choice. These twojobs[] entries use the first two:
t1 may ride v1 or v2, and costs 15.0 more on v1 than on v2. t2
rides v1 or is left unassigned.
From hardest to softest:
eligible_vehiclesis a hard whitelist (allowed) or blacklist (excluded). Set one, not both, and do not send an emptyallowedlist. A task whose lists rule out every vehicle is returned witheligible_vehicles. To restrict a task to one vehicle, list only that vehicle inallowed: any of its shifts may serve it. The search may still move such a task within that vehicle’s routes; to freeze stops that are already dispatched, useplan.routes[].locked_count.preferencesis soft. Each{ vehicle, violation_cost }entry is priced on its own: a task that lists several preferred vehicles pays for every listed vehicle that did not serve it, even if another one did. Aviolation_costis0to21474.83647, and the entries of one task must also sum to at most21474.83647. Do not combine a preference with anallowedlist that leaves out the preferred vehicle: the preference can never be met and its cost is paid on every solve.assignment_costsis soft too, and prices the other side: each{ vehicle, cost }entry is charged once when that vehicle (on any of its shifts) serves the task, and a vehicle not listed costs0. A task rated expensive on every vehicle is the first one dropped when not everything fits. Acostis0to21474.83647.
score.components.preferences and assignment
costs in score.components.assignment. For a rule that must always hold, use
eligible_vehicles instead of a large violation_cost.
Common 400 cases: a vehicle id that matches no vehicle, in any of these
fields; allowed and excluded set together; an empty allowed list; a
vehicle listed twice in preferences or assignment_costs; a violation_cost or cost that is
negative or above the maximum.
The same fields are available on shipments[], where they apply to the pair.
Priority and optional tasks
Every task is must-serve unless it prices its own drop. Two fields control what happens when not everything fits; these are twojobs[] entries:
integer
default:"1"
A rank among must-serve tasks, from
1 to 4294967295. When not all of them fit, the solver
keeps the summed priority of the served tasks as high as possible before it
looks at any cost. With every task at the default this means serving as many
as possible. It is a rank, not money, and never makes a task droppable. The
summed priority of the must-serve tasks left out is reported as
summary.score.unassigned_priority.number
What leaving the task unserved costs, in the same currency as
objective.costs, from 0 to 1,000,000,000. This is the only way to make
a task optional: it is served while serving it costs less than the fee. Fees the plan chose to pay
are reported in score.components.drop_costs. On a shipment the fee applies
to the pair and is charged once. A task dropped for its fee is listed in
unassigned[] with category: "priced_out".400: priority: 0; priority together with
drop_cost; a negative drop_cost; drop_cost on any task while a
positive objective.per_delay_workday is set (not_supported).
A one-vehicle allowed list does not make a task must-serve: a task
restricted that way that does not fit still comes back in unassigned[], and pays its
drop_cost if it has one.
Route limits
limits sits on the vehicle, and a shift can override max_distance_m,
max_route_duration_s and max_stops in its own limits. This is a
vehicles[] entry:
max_route_duration_s caps the whole route span, the
same quantity as the response’s routes[].time.span_s: travel, service,
waiting and depot handling all count. max_stops counts stops: a job is one,
a shipment two; start, end, break and reload stops in routes[].stops do
not count. max_distance_m and max_route_duration_s must be 0 to
2,147,483,647 at either level (out_of_range otherwise), and max_stops an
integer from 0 to 2,147,483,647, as in limits.periods and quotas.
max_leg_travel_s caps any single travel leg instead of the whole
route: the travel_s reported on each stop, including the leg from the
start to the first task and from the last task to end. Only travel counts
(setup, service and waiting do not), and speed_factor applies first. A leg
made free by free_start_travel or free_end_travel, or absent on an open
start or end, is exempt. It is set on the vehicle only, and must be between
1 and 2,147,483,647: 0 is a 400 with out_of_range, not “no cap”.
A task kept out by max_distance_m, max_route_duration_s or max_stops is
returned in unassigned[] with max_distance, max_route_duration or max_stops,
and one kept out by max_leg_travel_s with max_leg_travel.
The leg cap is enforced as reachability: no route may use a
leg over the cap, as with a leg that has no road connection, but the two are
reported apart. On /evaluate, a leg over the cap reports a max_leg_travel
violation: on the stop it leads to, where the message gives the leg’s travel
time and the cap, or on the route for the leg to end. A leg with no road
reports unreachable instead.
Period limits
limits.periods[] caps a vehicle’s totals across all of its shifts in a
period. This is a limits object:
every: "day" or every: "week" (weeks run
Monday to Sunday), or one explicit from/to window with to exclusive.
Each shift counts toward the one period that contains its from. For a
recurring period the date is read in the UTC offset that from is written
with; see How days are counted.
Set exactly one of
every or from + to, and at least one maximum, each
0 or more. A from/to window must end after it starts and contain at
least one of the vehicle’s shift starts; a period inside a single shift is
not supported (not_supported). Anything else is a 400. A recurring period
in which none of the vehicle’s shifts start is skipped. The
third entry above blocks the vehicle on 3 April: max_stops: 0 for the shifts
that start that day.
A task kept out by a period’s max_route_duration_s, max_distance_m or
max_stops is returned in unassigned[] with period_limit, the code
/evaluate uses: the period leaves each of its shifts that much less room.
Where the vehicle’s own limit is what the task would breach, the reason is
that limit’s code instead.
On /evaluate, a plan over a period’s cap reports one period_limit
violation, on the route of the period’s first used shift.
Quotas
quotas[] is a top-level field of problem. Each entry caps how many stops
of the listed tasks the listed vehicles serve together, for example
a contractor pool that shares one install quota. A quota with one vehicle is
a per-vehicle cap on those jobs. This is a quotas array:
id,vehicles,tasksandmax_stopsare required.taskstakes job and shipment ids.fromandtoare optional and go together. A stop counts when its service starts in[from, to)(arrival + wait_sin the response when no windowed break is taken on arrival there); no calendar day is involved, and a window can sit inside one shift. Without a window, every stop counts.- Stops count as
max_stopscounts them: a job is one stop, a shipment two. - The cap is hard: a job over it is left unassigned with
quota. - On
/evaluate, a plan over a quota reports onequotaviolation, on the route of its first used member shift.
400 cases: a duplicate quota id; an empty vehicles or tasks
list; an unknown vehicle, job or shipment id; an id listed twice in one list;
a window with only one end; a from or to that is not a timestamp; a to
that is not after from; and a window that overlaps no shift of the quota’s
vehicles.
Relations
relations[] ties tasks to each other. There are ten types, and all ten are
hard: a relation has no cost field. This is a relations array:
sequence: members that share a route appear on it in the listed order; other stops may sit between them. It does not by itself put the members on the same route. Members are listed instops. A single entry instopspins that job as the first stop of its route.consecutive: likesequence, and no other stop may sit between two neighbouring members that are served on the same route (V2DIRECT_SEQUENCE). If a member is unassigned it is skipped, so only listed neighbours that are both served are adjacent. Members on different routes are unconstrained: addsame_routeover the samestopsto keep them together. A stop has one successor and one predecessor across allconsecutiverelations ([a, b]and[b, c]may overlap,[a, b]and[a, c]are a400), andstopsneeds at least two entries. To keep a shipment’s two legs back to back, listx:pickupandx:delivery. Name a chain in anall_or_nonerelation to serve it whole or not at all.tag_order: orders tasks by tag instead of by id.tagslists at least two tags; on every route, every task taggedtags[0]precedes every task taggedtags[1], and so on. Tasks carrying none of the listed tags may sit anywhere, and a listed tag no task carries is accepted. A tag that selects a shipment is a400. A breach is reported astag_order.same_vehicle: members are served by the same vehicle, on any of its shifts. Select members withstopsor with onetag, not both.same_day: members are served on shifts whosefromfalls on the same calendar date, read in the offset eachfromis written with. The day belongs to the shift, not to the time of the stop; see How days are counted. Select members withstopsor with onetag, not both.same_route: members are served on one route: one shift of one vehicle (V2SAME_TRIP). Reloads within the shift do not split it. Select members withstopsor with onetag, not both. Jobs only: a shipment named instopsor carrying thetagis a400.synchronized: members start withinmax_spread_sseconds of each other, on any vehicles.max_spread_sis required and>= 0;0means exactly together. Add"on": "departure"to synchronise the ends of service instead of the starts. Select members withstops, up to eight members. The schedule holds an early member back; the hold is reported on the stop ashold_s.time_lag: a directional gap between two jobs on any vehicles. The time fromfromtotomust lie betweenmin_sandmax_sseconds. Each end is astopplus an optionalon, which isservice_start(default) ordeparture.min_sdefaults to0, an absentmax_smeans no upper bound, and both may be negative.from.stopandto.stopmust differ,max_smust not be belowmin_s, and both ends must be jobs: a shipment id or a shipment leg is a400. Chain more than two jobs with one relation per consecutive pair.compatible_vehicles: every two different vehicles that serve the members must be compatible undervehicles[].compatible_with. A vehicle without the field pairs with any vehicle; a list is a veto (neither list may exclude the other); a vehicle always pairs with itself, so list only its own id to pair it with no other. Members arestopsonly, 2 to 16, each also named by asynchronizedortime_lagrelation. Tasks left without a compatible placement are unassigned.all_or_none: either every member is served or none is. It sets no timing of its own. Members arestops, at least two; a member may be a shipment id, which stands for both legs, and a shipment-leg id is a400(name the shipment). Members are coupled by asynchronizedortime_lagrelation, or the group’s stops (a shipment counted as its two legs) are exactly oneconsecutivechain with no member coupled bysynchronizedortime_lag, directly or through asequenceorconsecutiverelation. The chain form keeps a shipment’s legs and the jobs between them together: a towx:pickup, a jobtandx:delivery, chained withconsecutiveand kept on one route withsame_route, is served whole or not at all. Overlapping relations chain: one on A and B plus one on B and C serves all three or none of them. The rule is switched off for a whole group, chained members included, once one of its served members sits inside a plan route’slocked_countor, on solve, is apinnedplan stop. A member that could be served but is held back because another cannot is unassigned with the reasonall_or_none. On/evaluate, a plan that serves only some members reports anall_or_noneviolation on each served member.
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.
Common 400 cases:
- a task id that does not exist, or a
tagonsame_vehicle,same_dayorsame_routethat no task carries - a relation with no member selector, or with two of them (
stopstogether withtag) - a
synchronizedrelation withoutmax_spread_s, with a negative one, or with more than eight members - a job listed twice in one
sequence,same_vehicle,same_day,same_routeorsynchronizedrelation on: "arrival"on asynchronizedrelation or atime_lagend (service_startanddepartureare the only values there)synchronizedandtime_lagrelations, and thesequencerelations joining them, that link more than 16 tasks into one group (see Limits)- a job listed in more than one
same_vehiclerelation, more than onesame_dayrelation, more than onesame_routerelation, or more than onesynchronizedrelation (merge them into one); a job may sit in any number ofsequenceandtime_lagrelations - more than one
tag_orderrelation, fewer than twotags, a tag listed twice, or a task that carries more than one of the listed tags - a
time_lagwhose two ends are the same job, whosemax_sis belowmin_s, or that names a shipment - a
same_routerelation that names or selects a shipment - a shipment leg coupled to other tasks by a
synchronizedortime_lagrelation, directly or through asequencewith a member that is - a
sequencethat puts a delivery before its own pickup, or names a delivery alone - a
compatible_vehiclesrelation with fewer than 2 or more than 16stops(invalid_valueandlimit_exceeded), that names a shipment, or that names a job which nosynchronizedortime_lagrelation also names (bothnot_supported) - an
all_or_nonerelation with fewer than 2stops(invalid_value), that names a shipment leg, that names a shipment or a job outside a coupled group or a singleconsecutivechain, whose chain runs on past the group, or whose chain member is coupled bysynchronizedortime_lag, directly or through asequenceorconsecutiverelation (allnot_supported) - a
vehicles[].compatible_withthat is empty (invalid_value) or names a vehicle that does not exist (unknown_reference) - an unrecognised
type, reported witherrors[].code: "invalid_type"
Shipments in relations
To relate a shipment, name one of its legs by its stop id: the leg’spickup.id or delivery.id, by default "{shipment}:pickup" and
"{shipment}:delivery". The relation then binds that leg like a job.
["s1:pickup", "j1", "s1:delivery"] in a sequence serves j1 between the
two legs. A leg cannot be coupled across routes: a leg named by a
synchronized or time_lag relation, or in a sequence with a member that
is, is a 400.
A shipment’s own rule, delivery after pickup on one vehicle, needs no relation.
To tie a shipment to a vehicle, use its
eligible_vehicles.
Shipments
Ashipments[] entry is a pickup and a delivery served by one vehicle:
relations[] entry. load is carried
on the vehicle between the two legs.
Each leg accepts its own id, location (required), setup_duration_s,
service_duration_s, time_windows and target. Load is the shipment’s own
load, not a per-leg field. A leg id defaults to
{shipment}:pickup or {shipment}:delivery and is what the response reports
on that stop. A leg id, set or defaulted, that equals any job, shipment or
other leg id is a 400 with duplicate_id.
drop_cost, priority, required_skills, tags, eligible_vehicles,
preferences, assignment_costs and service_factors sit
on the shipment and apply to the pair, priced once.
target is measured on the delivery leg. A leg’s own pickup.target or
delivery.target sets a target on that leg; target with delivery.target is
a 400.
Driver breaks
Breaks are listed inshifts[].breaks. Each has a type: windowed,
triggered or fixed. This shifts[] entry
carries three of them. Each may take an optional id, which comes back on its
break stop:
Break in a time window
Awindowed break lasts duration_s seconds and must
start no earlier than the window’s from and no later than its to. It
is taken wherever the vehicle is at that moment. It takes exactly one
time_window.
from idles until it. The stop’s time window then bounds the start
of service after the break. With a 09:00–09:05 window, an arrival at
08:02 and a break taken 10:00–10:30 on arrival, service starts at 10:30: the
stop misses its window, so a hard window leaves it unassigned and /evaluate
reports a time_window violation, and lateness_s is 5100 (10:30 − 09:05).
The break stop is listed before the stop it was taken at, so that stop’s
arrival can precede the break’s.
A windowed break has no location: sending one is a 400 (not_supported,
pointer .../breaks/{k}/location), because the break is always taken where
the vehicle is.
Break after driving time
Atriggered break with after: "drive" is taken once the driving
time since the last break, or since the start of the shift, exceeds
threshold_s. The count starts again after the break. This is the EU
561/2006 rule of 45 minutes after 4 h 30 of driving:
Break after duty time
Withafter: "duty" the count is driving time plus service time.
This break is taken after six hours of work:
Fixed off-duty interval
Afixed break is an interval with a set from and to. No stop is
served inside it: a stop whose service would not finish before from is
served from to instead, except the service of a job with
break_interruptible: true (below), which pauses for the break. Travel is not
split around it, so a drive still under way at from overlaps the break, and
the overlap counts in both time.travel_s and time.break_s. It takes those
two fields only: no duration_s, no location.
Breaks during a long job
By default a break never falls inside a job’s service: the whole service is moved after the break, or the break is taken before the job starts. Setbreak_interruptible: true on a job whose work may pause. A fixed,
windowed or duty-triggered break that falls inside its service then
pauses it, and the work resumes when the break ends. The job ends the
break’s length later. A drive-triggered break never pauses a job, because
it counts driving time only. Shipment pickups and deliveries cannot be
interrupted.
fixed break from 12:00 to 13:00,
works until 12:00, pauses, and ends at 15:00. Without the flag the job has to
end by 12:00 or start at 13:00.
Breaks in the response
A break appears on its route as a stop of typebreak, in sequence between
the stops it falls between:
arrival is when the break starts, departure when it ends, and service_s
its length in seconds. The stop also carries load_after when the request
uses capacity. It carries the id of the request break when that break has one
(breaks[].id, 1 to 128 characters, unique within the shift), and no
location, also when the request set a location on the break.
A break that pauses an interruptible job has no break stop of its own. The
job stop reports the paused seconds in break_s, and its departure is that
much later. The route’s time.break_s includes them.
Break rules
Common400 cases:
duration_sorthreshold_sthat is not greater than0, or athreshold_sabove 2,147,483,647- a window or a
fixedbreak whosefromis after itsto - a field the break type does not define, such as
locationon afixedbreak (unknown_field) - a
locationon awindowedbreak (not_supported)
location and no resets_accumulator: the count
always starts again after a break, and the break is taken wherever the vehicle
is. A shipment leg has no break_interruptible. Sending any of them,
or a windows list on a windowed break, is unknown_field.
Multi-trip reloads
A vehicle may return to a depot mid-shift to empty or restock and start a fresh trip. Name the depots in the shift’sreloads:
reloadslistsdepots[].idvalues, each at most once. An unknown id, or one listed twice, is a400.- Capacity is enforced per trip. Each reload appears in the response as a
stop of type
reloadcarrying the depot id. max_tripscaps trips, not reloads: trips = reloads used + 1, across all reload depots of the shift, not counting the final unload ofunload_before_end(below). It must be at least1; without it, trips are not capped.- Whatever
max_tripssays, one shift reloads at one depot at most eight times. List more than one reload depot to go beyond that. Aplanyou send whose route reloads at one depot more than eight times is a400. - Each reload takes the depot’s
service_duration_sand is charged itsper_reload, reported inscore.components.reloads. shifts[].initial_loaddeclares stock already on the vehicle at shift start. It occupies capacity on the first trip only and is cleared at the first reload. A value above the capacity in that dimension (the shift’s, else the vehicle’s) is rejected with a400. At capacity the first trip has no room, so the route can start with a reload at one of the shift’sreloadsto unload before its first task.shifts[].unload_before_end: truekeeps the vehicle from ending the shift loaded, as a waste truck must empty at a tip before returning to its yard. When the last trip carries load, the route ends with one morereloadstop at the shift’sreloadsdepot the solver finds cheapest, then drives toend. This stop takes the depot’sservice_duration_sandper_reload, and does not count towardmax_trips. A route whose last trip carries no load ends without it.truewithoutreloadsis a400. In aplanyou evaluate, a route that ends loaded reports anunload_before_endviolation, and areloadas the last stop is accepted on such a shift only.depots[].max_intakecaps what a depot receives across the whole request, per named dimension, as a tip site accepts only so much waste. Counted is the load on board on arrival at eachreloadthere, including the final tip ofunload_before_end, and the load on board at the end of a shift whoseendis that depot (anunload_before_endshift unloads before itsendinstead). It is a total over every shift and day, not a daily limit. A dimension left out is uncapped, and0means the depot takes none of it. The solver sends trips to another of theirreloadsdepots when one is full, and leaves a task unassigned (reasonmax_intake) when none has room. A cap on a depot no shift reloads at or ends at (withoutunload_before_end), a negative amount or an undeclared dimension is a400. In aplanyou evaluate, a depot over its cap reports onemax_intakeviolation per dimension, with theexcess, on the first route that unloads there.
Travel and service time
These fields do not forbid anything on their own, but they change how long a route takes, and so whether time windows and shift ends can be met.number
default:"1.0"
Travel time is divided by this factor: above
1.0 is a faster vehicle,
below is slower. Must be greater than 0 and at most 5.number
default:"1.0"
Service time is divided by this factor: above
1.0 is a faster worker. Same
range as speed_factor. shifts[].service_factor replaces it for one
shift, and a task’s service_factors[].factor multiplies whichever applies.
It scales a task’s service_duration_s only, never setup time, depot
handling, waiting or breaks. V2’s durationModifier went the other way: it multiplied the duration (0.8 = 20% faster). When migrating, send 1 / durationModifier as the task’s service_factors[].factor.array
On a job or a shipment: per-vehicle overrides for one task, as
{ "vehicle": "ann", "factor": 1.25 } entries. The factor multiplies the
shift’s factor if the shift sets one, else the vehicle’s:
scheduled service time = service_duration_s / (shift or vehicle factor × task factor).
On a shipment it scales both legs. A vehicle not listed works at its own
rate. Each factor must be greater than 0 and at most 5. An unknown
vehicle id, or a vehicle listed twice, is a 400.integer
On a job or shipment leg: time paid once on arrival at a location, on top of
service_duration_s. It is skipped when the previous stop on the route is at
the same location, so co-located stops share one setup.stops[].service_s, which differs from service_duration_s whenever a factor
applies.
Grouping
objective.grouping keeps nearby tasks together without restricting the
plan. Tasks within radius_m of each other form a group, and every entry
into a group from a task outside it pays per_entry, the first entry
included. Entering from the shift’s start or from a reload is free. This is an objective object:
per_entry is required and must be between 0 and 1000. radius_m
defaults to 180 and must be positive. distance_metric is straight_line
(default) or road. The total is reported in score.components.grouping.
Reachability
When the road network has no connection between two locations, no route may use that leg. The rule has no request field of its own; a vehicle’slimits.max_leg_travel_s adds to
it by ruling out every leg that takes longer than the cap. A task that can
only be reached over a missing road comes back unassigned with
unreachable: check its coordinate, which is [longitude, latitude]. One
that can only be reached over a leg longer than the cap comes back with
max_leg_travel: check the leg cap on the vehicles. See
Distance matrices for how travel
data is looked up.
What a broken hard rule looks like
A solve response never contains a violated hard rule. A task that cannot be placed is listed inunassigned[]:
categorysays what kind of problem it is:impossible(no vehicle can ever serve the task),blocked(it does not fit alongside the tasks the plan serves, or its all-or-none crew cannot be placed together),priced_out(itsdrop_costwas cheaper than serving it) ortime_limit(a slot exists that the search did not use in its time budget).reasons[]names every rule that refused the task on some vehicle, oneconstrainteach. The sections above give the constraint for each rule.relaxations[]suggests the smallest change that would let the task fit, when one exists.
- Every rule, once. When different rules block different vehicles, each
is listed: capacity on some vehicles and time windows on others is
capacityandtime_window.unknownmeans no rule was recorded. - A window no shift overlaps. The constraint is
time_windowwith categoryimpossible. Check the dates and UTC offsets of the window and the shifts. - Relations name themselves.
same_vehicle,same_dayandsame_routenarrow a task to the vehicle, day or route its already-placed partners use; the reasons list that relation, plus any rule that blocks the task there. A task kept out bysequencereportssequence. priced_outandtime_limitname no constraint. No rule kept the task out: their one reason names noconstraint. Branch oncategory.
relaxations[] entry has five fields:
The list is empty for rules with no such fix, for example a skills mismatch.
To check a plan you built yourself, send it to
/v3/routing/evaluate. There, broken hard rules are
reported as violations[] on the stop or route instead.
The V3 API model
How the full request is structured.
Objective function
How the soft rules above are priced and ranked.
Errors
The
400 error body, item codes and pointers.API reference
Every field of the request and response schemas.