V3 is in beta. General availability (GA) is planned for January 2027.
POST /v3/routing/solve, the priority field you use to rank required tasks, and the cost model behind summary.score.
On this page a task is a job or a shipment.
How plans are compared
Every plan has ascore with three figures, compared in a fixed order. A later figure only matters when the earlier ones are equal.
violations: the number of hard-constraint violations. A solve result never breaks a hard constraint, so this is always0on/v3/routing/solve. It can be non-zero on/v3/routing/evaluate, which scores a plan you supply.unserved_priority: the summedpriorityof the required tasks left unassigned. Less is better, and no amount of money outweighs it: the solver never leaves a required task out to save cost.cost: money, in your currency. Travel, time, vehicle charges, every soft-constraint price, and thedrop_feeof each optional task the plan chose not to serve.
summary.score. currency echoes objective.currency and is omitted when you did not send one.
Priority: which tasks are kept when not all fit
Every task is required unless it carries adrop_fee. When the fleet cannot serve all required tasks, priority decides which ones the solver keeps.
integer
default:"1"
Importance of a required task,
1 or higher. Also available as shipments[].priority. The solver maximizes the summed priority of the tasks it serves before it looks at cost.boiler-repair ranks higher: it leaves routine-check in unassigned[] with unserved_priority: 1, where the alternative would score 5.
- A rank, not money. Priority is never traded against cost, and it never makes a task optional.
- It is a sum. Leaving out two priority-1 tasks (
unserved_priority: 2) ranks better than leaving out one priority-3 task (unserved_priority: 3). With every priority at the default of1,unserved_priorityis the number of required tasks left out. A shipment counts once. - Not with
drop_fee. A task with adrop_feeis optional and priced in money, so it has no priority: sending both is rejected with HTTP 400. So ispriority: 0. - Optional tasks are not counted here. A task left out for its
drop_feeadds the fee tocost(undercomponents.drop_fees), not tounserved_priority.
What you set: costs at three levels
objective.costs, vehicles[].costs and shifts[].costs take the same five fields. Each field resolves on its own: the shift’s value, else the vehicle’s, else the fleet’s, else the default. A value set lower down replaces the one above it; nothing is added on top, and every field is reported under the same score.components key at every level.
Two settings sit on
objective itself, not in costs:
A worked example, with no
objective sent so the default fleet rates apply:
time.span_s: 7200) and drives 40 km, the score reads:
time is the vehicle’s own 30 per hour, which replaces the fleet’s 35, distance is 40 km at the fleet’s 0.20, and fixed is the fleet’s default of 0.
time is 59.976, not 60.0. Hourly rates are rounded to a per-second rate with five decimals, and per-kilometre rates to a per-metre rate with five decimals, so 30 per hour is charged as 0.00833 per second. Expect differences of a few cents when you re-derive a cost by hand. A non-zero rate too small to survive that rounding is rejected with HTTP 400.Combinations that are rejected
Each of these returns HTTP 400 with a message describing the problem:objective.per_late_hour: 0.0. Omit the field to keep time windows hard. Every other rate accepts0.0as “priced at zero”.- A negative rate.
Reading summary.score.components
All thirteen components are always present, 0.0 when nothing priced them, and they add up to score.cost exactly.
time is priced on each route’s span, reported as routes[].time.span_s. summary.time.travel_s and routes[].time.travel_s are driving time only, so time cannot be re-derived from them.Because the span already includes waiting, an idle second is charged by per_route_hour and again by per_wait_hour when both are set. Set per_wait_hour to the premium you want on top of the hourly rate, not to the full cost of idling.If you set nothing: the V3 defaults
The defaults are plain numbers in whatever currency you work in. Each rate keeps its default until you set that rate: sending only
per_travel_km leaves per_route_hour at 35.
Vehicle count is free until you price it
By default nothing is charged for using a vehicle, so the solver opens another route whenever that lowers travel cost, however small the saving. There is no flag for this: the vehicle count goes down only when vehicles have a price. To make fleet size matter, price it. Fleet-wide, in theobjective object:
vehicles[].costs.fixed, when vehicles really cost different amounts. A vehicle’s own fixed replaces the fleet fixed for that vehicle; its other rates leave the fleet fixed in place.
Once a price exists, the trade-off is ordinary arithmetic. At 150 per vehicle with those travel rates, an extra route pays for itself only when it saves more than about 750 km (750 × 0.20 = 150) or about 4.3 hours (4.3 × 35 ≈ 150) elsewhere. A smaller saving does not, and the solver keeps the routes consolidated.
Droppable tasks: drop_fee
The only way to make a task optional is to say what leaving it unserved costs you.
drop_fee: the price of not serving the task, in the same currency asobjective.costs. It is part ofcost, so the task is served exactly while serving it costs less than the fee. Fees the plan paid are reported incomponents.drop_fees. On a shipment the fee covers the pair and is charged once.- A task without a
drop_feeis required.
job-1 is required. job-2 may be left out for 1000 when serving it would cost more than that. Pricing is per task; there is no fleet-wide drop price.
Because a paid fee is inside cost, costs stay comparable between plans that serve different sets of optional tasks.
Diagnosing unassigned tasks: unassigned[]
Each task left out comes back in unassigned[] with a category (impossible, blocked, priced_out or search_limit), machine-readable reasons[], and relaxations[] suggesting the smallest change that would let it fit. Cost tuning does not rescue a task that a hard constraint blocks, so read these before you adjust rates. How constraints work explains how to read an entry, and Errors lists every category and reason code.
Tuning for business goals
”I want the fewest vehicles, and I know what a vehicle costs me”
Setvehicles[].costs.fixed to your real cost per vehicle per shift, or objective.costs.fixed for one fleet-wide figure. The larger it is relative to the travel rates, the more the solver consolidates.
”I want the cheapest routes and do not care about vehicle count”
Set nothing per vehicle. That is the default.”Driver time matters more than distance” (or the reverse)
Setper_travel_km to 0.0 and per_route_hour to your real hourly rate, or the other way round.
”Some time windows may be missed, at a price”
For individual tasks, settime_windows[].per_late_hour on the task’s window. For every task, set objective.per_late_hour. Either way the rate must be greater than zero; to keep windows hard, omit it. Lateness pricing applies to task windows only: a shift’s to (plus max_overtime_s) stays a hard limit. See How constraints work for all the soft levers.
”Some required jobs matter more than others”
Give them a higherpriority. When not everything fits, the solver keeps the highest summed priority.
”Some jobs are nice-to-have”
Give each adrop_fee: the amount you would rather pay than force the task onto an expensive route.
Related topics
How constraints work
Hard constraints, the soft levers that feed
cost, and the unassigned[] diagnosisConstraints
Every constraint field, with the JSON to send
Evaluate
Score a plan of your own, including its violations
Performance guide
The levers that affect solve time rather than plan shape