V3 is in beta. General availability (GA) is planned for January 2027.
POST /v3/routing/evaluate scores any plan you build by hand and reports each broken rule as a typed violation with how far over the limit it is. You never have to guess which rule, on which field, kept a task out.
Three steps, in order: recognise, diagnose, fix. Every response excerpt below is real output from a four-task, one-vehicle problem solved with travel.engine: "straight_line".
Recognise
It is a 200, not an error
A 4xx means the request itself was wrong and nothing was planned; see Errors. A plan that leaves tasks out is a 200. Check summary.tasks on every solve response:
unassigned[], one entry per task on no route:
Pass
unassigned[] through to the people who maintain the data: message is written for them, and pointer names the exact field in the request you sent.
On evaluate: typed violations
/v3/routing/evaluate does not search, so a plan you send can break hard rules. Each broken rule is a violations[] entry on the stop or route that breaks it, and summary.score.violation_count counts them. The entry carries a stable code, and for the numeric rules an excess with its unit:
unassigned[] shape as on solve, but category then says whether the task could be added to the plan as it stands: time_limit means it fits and is unassigned only because your plan omits it. See Violations and What unassigned means on evaluate.
What the solver accepts without telling you
A task is unassigned either because a hard rule refused it or because serving it would cost more than itsdrop_cost (category: "priced_out"). Everything below is accepted and priced, or accepted and free, so a plan with unassigned: [] and violation_count: 0 can still hold outcomes you did not intend. Check the field in the right-hand column.
To make a priced rule hard again, remove the rate or the overtime grant. Removing
drop_cost does not make a task unassigned: it makes the task required, so one that still cannot be served comes back as impossible or blocked instead of priced_out.
Diagnose
1
Group the unassigned tasks by category
category decides which of the fixes below can work at all. impossible means no vehicle can ever serve the task: fix the request. blocked means a vehicle could serve it alone but not alongside the planned tasks: add capacity or working time, or relax a rule. priced_out means a slot exists and serving it costs at least its drop_cost. Only time_limit is a reason to give the search more time. In the response above both are impossible, so no amount of search time changes it. Categories has the full definitions.2
Map each reason code to the field that caused it
Read
reasons[].constraint and go to the field it names. A task lists each rule once, so two entries mean two different rules refused it on different vehicles.New codes can be added as new constraints ship. Branch on the ones you handle and treat the rest as
unknown.3
Apply a relaxation literally, then re-solve
When
relaxations[] is non-empty, it names the field and the amount: above, raising van-1’s capacity.weight by 15, or lowering job-heavy’s pickup_load.weight by 15, is enough. The hint is best-effort and about this task alone, so re-solve and read unassigned[] again.4
Evaluate a plan you built by hand
When a reason code is not enough, force the placement and let evaluate say exactly what breaks. Send the same The stop excerpt under On evaluate is this call’s answer:
problem with a plan that puts the task where you expect it:job-early’s window closes at 08:05, the van arrives at 08:11:38, so the violation is time_window with excess: 398 seconds. That number is the amount to widen the window by, or the service time to shave off job-ok. Evaluate does not search, so the answer carries no search noise.5
Ask suggest for the cheapest slot
To price a task against a plan you already have, send
problem and plan to /v3/routing/suggest with the task left out of plan. Each feasible insertion carries its cost delta, and a task with no feasible position says so:latest_arrival is the slot’s slack: job-later can arrive as late as 11:37 and the route still holds. Only reason.code: "infeasible" is final. See When a task gets no insertions.6
Change one thing at a time, with the seed fixed
Keep
options.seed constant between runs, so a difference comes from your change and not from a different random walk, and compare in the solver’s order: lower score.unassigned_priority first, then lower score.cost. A 90-minute shift with three 50-minute jobs:Had the second run also widened a window, you could not tell which change placed the second job. See Comparing runs.
Fix
Ranked by how little they change about the operation.impossible and blocked tasks need 1 to 3, priced_out needs 4, and only time_limit needs 5.
1. Relax the rule
Soften one window. Field:time_windows[].per_late_hour, on a task with exactly one window.
components.lateness instead of an unassigned task, and nothing in unassigned[] or violation_count flags it; watch stops[].lateness_s. objective.per_late_hour softens every window at once, at the same loss of visibility.
Widen the window by the relaxations[].amount the solver gave you. Field: time_windows[].to (or .from).
drop_cost, in your currency. It cannot be combined with priority.
priced_out and leaves unassigned_required untouched. Set the fee to what a missed visit costs you: a run with 1.0 dropped the task rather than drive a 45 km round trip.
When every task must stay required, rank them with priority instead, so the solver chooses which to keep when not all fit. See Priority.
2. Add capacity
A bigger or an extra vehicle. Field:vehicles[].capacity, or a new vehicles[] entry.
costs.per_route if it should open only when needed.
More working time. Field: shifts[].max_overtime_s, priced with costs.per_overtime_hour.
to + max_overtime_s, so shift_end can still refuse a task. Without the rate there is no overtime premium, only the per_route_hour charge on those hours, so the solver uses all of it: the run above ran 796 seconds over with components.overtime at 0.0. A second shifts[] entry on another date adds working time without a new vehicle.
3. Split the task
Field: twojobs[] entries in place of one, each with part of the load or the service time.
max_stops. Add a same_route relation when both halves must travel together, or leave them free so two vehicles can share the work. A load no single vehicle can carry is the one impossible case this fixes without touching the fleet.
4. Change what the solver trades off
Forpriced_out, raise the drop_cost until serving is the cheaper option, or remove it to make the task required. When vehicles carry a per_route, a dropped task may be paying for the route it would open alone: compare the fee against that rate.
per_route for one task also opens vehicles elsewhere. Rates never rescue a task a hard rule refuses: impossible and blocked tasks stay out whatever the costs say.
5. Raise time_limit_s
Field: options.time_limit_s, at most 60 on POST /v3/routing/solve and 3600 on an async run. Only for category: "time_limit".
search_progress to see where the curve flattens, and try another seed before buying more time. See The time budget.
Errors
Every category and reason code, with what to check.
Evaluate
Score a plan you built and read its typed violations.
Constraints
Every hard and soft rule, with the JSON to send.
Solution quality
Coverage, cost and how to compare two runs.