Skip to main content
V3 is in beta. General availability (GA) is planned for January 2027.
A solve response tells you how good its plan is in three layers: how much of the work it covers, what it costs, and what the routes look like. This page walks through the fields for each, the figures you can derive from them, and what to change in the request when a number is not where you want it. A task is a job or a shipment. This page says “job” for both: a shipment is reported the same way, with the shipment’s 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:
  1. violations: hard-constraint violations. Always 0 on a solve response.
  2. unserved_priority: the summed priority of the must-serve jobs left unassigned. Lower is better.
  3. cost: the total cost of the plan in your currency. Lower is better.
A plan that serves more priority always beats one that is merely cheaper. Use the same order when you compare runs yourself. See Objective function for how the cost is built up.

Coverage

The 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.
To measure coverage, read 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.
Both fields exist on 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:
Tasks served is the figure from Coverage. A rising drop-fee share across similar requests means the solver is increasingly choosing to pay fees instead of serving jobs. Check your drop_fee values against your cost rates before treating it as a routing problem.
drop_fees only covers jobs that carry a drop_fee. A must-serve job left unassigned adds nothing to cost; it shows up in unserved_priority instead. Never judge coverage from the cost breakdown alone.

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.
Here the vehicle drives for 33 minutes (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_m is the route’s travel distance in meters; summary.distance_m is the fleet total. distance_m divided by tasks served is a useful density figure, but only when comparing runs over the same set of jobs.
  • time.overtime_s is the time the route runs past its shift’s to, 0 when it finishes in time. Overtime is only possible on a shift that sets max_overtime_s. For a fleet-wide figure read summary.time.overtime_s rather than summing the routes.
  • load_peak is the highest load the route carries, per capacity dimension, when the request uses capacities. Compare it with the vehicle’s capacity to 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 in routes[].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_hour on 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.
See Time windows for the rules. Lateness charges are reported in 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:
Use 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 (another seed, a longer time_limit_s, an edited request), compare the results in the solver’s own order:
  1. Lower score.unserved_priority wins.
  2. If tied, lower score.cost wins.
  3. 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

A hard window can force a long detour or an extra vehicle. If arriving a little late is acceptable for some jobs, a per-window per_late_hour lets the solver weigh the lateness against the saving.

Give the search more time

More time and different seeds both let the search explore more plans. See the Performance guide for how to tell when more time has stopped helping.

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 sent
  • summary.score.unserved_priority, score.cost, and the components you care about
  • summary.distance_m and summary.time.span_s
  • the count of unassigned[] entries per category
  • summary.timings.total_ms and timings.search_ms, to relate quality to the time you gave the search
Set targets from your own history and service levels: what counts as a good served share or on-time rate depends on your fleet and your customers.

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