Skip to main content
V3 is in beta. General availability (GA) is planned for January 2027.
Your hard constraints say which plans are allowed; the objective decides which allowed plan is best. This page explains that comparison for 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 a score with three figures, compared in a fixed order. A later figure only matters when the earlier ones are equal.
  1. violations: the number of hard-constraint violations. A solve result never breaks a hard constraint, so this is always 0 on /v3/routing/solve. It can be non-zero on /v3/routing/evaluate, which scores a plan you supply.
  2. unserved_priority: the summed priority of 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.
  3. cost: money, in your currency. Travel, time, vehicle charges, every soft-constraint price, and the drop_fee of each optional task the plan chose not to serve.
The object is returned as 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 a drop_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.
If only one of these two jobs fits, the plan that serves 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 of 1, unserved_priority is the number of required tasks left out. A shipment counts once.
  • Not with drop_fee. A task with a drop_fee is optional and priced in money, so it has no priority: sending both is rejected with HTTP 400. So is priority: 0.
  • Optional tasks are not counted here. A task left out for its drop_fee adds the fee to cost (under components.drop_fees), not to unserved_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:
For a route of this vehicle that spans two hours (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 accepts 0.0 as “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 the objective object:
Or per vehicle, with 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 as objective.costs. It is part of cost, so the task is served exactly while serving it costs less than the fee. Fees the plan paid are reported in components.drop_fees. On a shipment the fee covers the pair and is charged once.
  • A task without a drop_fee is 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”

Set vehicles[].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)

Set per_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, set time_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 higher priority. When not everything fits, the solver keeps the highest summed priority.

”Some jobs are nice-to-have”

Give each a drop_fee: the amount you would rather pay than force the task onto an expensive route.

How constraints work

Hard constraints, the soft levers that feed cost, and the unassigned[] diagnosis

Constraints

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