V3 is in beta. General availability (GA) is planned for January 2027.
id in unassigned[].job_id. The one place they differ is the two counters under Coverage.
How plans are ranked
summary.score is how the solver itself compares two plans, in strict order:
violations: hard-constraint violations. Always0on a solve response.unserved_priority: the summedpriorityof the must-serve jobs left unassigned. Lower is better.cost: the total cost of the plan in your currency. Lower is better.
Coverage
routes and plan arrays are left out of this example.
integer
Jobs and shipments on a route. A job is one task and a shipment is one task.
integer
The number of entries in
unassigned[]: one per unassigned job and one per unassigned shipment.array
One entry per unassigned job or shipment, with a
category, one or more reasons (a stable code plus a message), and relaxations: the smallest change to the request that would let the job fit, when a simple one exists.summary.tasks, which counts a job and a shipment as one task each:
Read category first
category says which kind of unassigned a job is, and so what to do about it. It is one of impossible, blocked, priced_out, or search_limit. Categories on the Errors page lists what each one means and the fix that goes with it.
Only search_limit is a reason to give the solver more time. Use category instead of the reason code to make that call: in the example above, job-47 carries the SOLVER_LIMIT reason code but its category is priced_out, so a longer search would leave it out again.
Priority and droppable jobs
Two job fields decide what happens when not everything fits:integer
default:"1"
The importance of a must-serve job,
1 or higher. When the fleet cannot serve every job, the solver maximizes the summed priority of the jobs it does serve before it looks at cost. With every job at the default, that means serving as many jobs as possible. priority is a rank, not money, and never makes a job optional. Setting it together with drop_fee is a 400.number
What leaving this job unserved costs you, in the same currency as your cost rates. This is the way to make a job optional: it is served only while serving it costs less than the fee. Fees the plan chose to pay are reported in
score.components.drop_fees and included in score.cost.shipments[] as well.
Because of priority, a lower tasks.unserved is not automatically the better plan: leaving out two priority-1 jobs beats leaving out one priority-5 job. Compare score.unserved_priority, not the count.
Cost
score.cost is the money the solver minimized, and score.components breaks it down. Every component is always present (0.0 when nothing was charged) and the components add up to cost.
Two figures worth deriving:
drop_fee values against your cost rates before treating it as a routing problem.
Distance and duration
Three fields are called “duration”. They measure different things:integer
Driving time only: the sum of the route’s
travel_s legs. It excludes service, waiting, and breaks.integer
The route’s start-to-finish span: from arrival at the start stop to departure from the end stop, covering driving, service, waiting, and breaks. This is what
per_route_hour is charged on and what vehicles[].limits.max_route_duration_s caps. Always at least time.travel_s.object
The sum of every route’s
time. summary.time.travel_s is driving time only; summary.time.span_s is total working time across the fleet.time.travel_s: 900 + 480 + 600 seconds) inside a route that spans 93 minutes (time.span_s: 08:00 to 09:33 local time). The difference is 45 minutes of waiting for job-1’s time window to open and 15 minutes of service.
The other route-level figures:
distance_mis the route’s travel distance in meters;summary.distance_mis the fleet total.distance_mdivided by tasks served is a useful density figure, but only when comparing runs over the same set of jobs.time.overtime_sis the time the route runs past its shift’sto,0when it finishes in time. Overtime is only possible on a shift that setsmax_overtime_s. For a fleet-wide figure readsummary.time.overtime_srather than summing the routes.load_peakis the highest load the route carries, per capacity dimension, when the request uses capacities. Compare it with the vehicle’scapacityto see how full the vehicle gets.
Fleet use
summary.vehicles_used counts the distinct vehicles that serve at least one task. summary.shifts_used counts the routes. A route is one vehicle on one shift, identified by routes[].vehicle and routes[].shift, so a vehicle with three shifts that all serve tasks adds 1 to vehicles_used and 3 to shifts_used.
Travel quality
summary.travel_estimated is true when Solvice Maps could not be reached and the plan was solved on estimated travel (straight-line distance with a detour factor). Treat such a plan as provisional: time-window feasibility and score.violations hold only against the estimate, and no routes[].polyline is returned. The field is absent when travel is road-routed. Solve again once the provider is back, or send options.travel_fallback: "fail" to get the error instead. See Distance matrices.
Timing at each stop
Each entry inroutes[].stops[] reports how its time was spent, as in the route example above:
string (RFC 3339)
When the vehicle physically arrives. Service starts at
service_start: after any windowed break taken on arrival, and after wait_s.string (RFC 3339)
When the vehicle leaves.
integer
Seconds spent waiting before service: for the time window to open, and for a
synchronized or time_lag partner on another vehicle. The partner part is also reported on its own as hold_s when it is non-zero.integer
Time on site, with the serving vehicle’s
service_factor already applied.integer
Driving time from the previous stop to this one, in seconds.
integer
Driving distance from the previous stop to this one, in meters.
integer
Seconds past the end of the stop’s time window,
0 when on time. It can only be non-zero on a soft window.integer
Seconds past the job’s
target.at. Present only on stops for jobs that set a target. A target is separate from the time window and never affects lateness_s.arrival, service_start and departure are written in the UTC offset of the route’s shift from: a shift starting at 2026-07-01T08:00:00+02:00 reports times like 2026-07-01T08:15:00+02:00, and a shift from in UTC reports Z. Each route uses its own shift’s offset, so two vehicles in different time zones each read in local time. Parse them as instants when you compare times across routes.
Fields that do not apply to a stop are omitted, not set to 0. Treat a missing field as “not applicable”.
A time window is hard by default: the solver never plans a late arrival, and a job that cannot be reached in time is left unassigned. A window becomes soft, allowing a priced late arrival, in one of two ways:
- Per window: set
time_windows[].per_late_houron a job with exactly one window. Only that job may run late, at that hourly rate. - Fleet-wide: set
objective.per_late_hour. Every window may run late at that rate, except windows carrying their own rate.
score.components.lateness.
Two rollups you can build across all stops:
Workload spread
To see how evenly work is divided, compare routes on their start-to-finish span:time.span_s, not time.travel_s: driving time alone misses service time, which dominates in service-heavy fleets. Counting job stops per route is the other common measure.
The solver minimizes cost; it has no objective for balancing workload across vehicles. To keep routes within bounds, cap them: limits.max_tasks limits the stops on a shift and vehicles[].limits.max_route_duration_s limits a route’s span. Both are hard limits. See Constraints.
Comparing runs
When you run the same problem more than once (anotherseed, a longer time_limit_s, an edited request), compare the results in the solver’s own order:
- Lower
score.unserved_prioritywins. - If tied, lower
score.costwins. - If still tied, use whatever matters to you operationally: total
time.span_s,overtime_s, workload spread.
score.cost already includes the drop fees the plan paid, so comparing totals is fair when jobs are droppable. Only compare cost between runs that use the same cost rates.
Improving a plan
Too many unassigned jobs
1
Group the unassigned jobs by category
impossible jobs need a request fix, blocked jobs need more capacity or working time, search_limit jobs need more search, and priced_out jobs are the outcome you priced. See Read category first.2
Apply the relaxations
For
blocked jobs, relaxations[] names the field to change and by how much: widen a time window by amount seconds, or reduce a demand (or add that much capacity to a vehicle).3
Decide what is optional
If some jobs are nice-to-have, give them a
drop_fee so the solver weighs them against cost. Rank the must-serve ones with priority so the most important are served first when not all fit.Cost or distance is too high
These three changes are independent, so you can combine them.Set your real cost rates
objective sits inside problem. Its costs tell the solver how to trade driving distance against working time. Without them the solver uses 35 per hour and 0.20 per km. Rates that reflect your own economics produce plans that are cheaper for you, not only shorter.
Allow priced lateness
per_late_hour lets the solver weigh the lateness against the saving.
Give the search more time
Tracking quality over time
To follow quality as a KPI, store a few fields from each response:summary.tasks.served,tasks.unserved, and the number of tasks you sentsummary.score.unserved_priority,score.cost, and thecomponentsyou care aboutsummary.distance_mandsummary.time.span_s- the count of
unassigned[]entries percategory summary.timings.total_msandtimings.search_ms, to relate quality to the time you gave the search
Related topics
Objective function
How the score is built and how to set cost rates
Constraints
The hard and soft rules behind every unassigned reason
Performance guide
Choosing a time budget and handling a busy service
Evaluate
Score a plan you built yourself with the same figures