V3 is in beta. General availability (GA) is planned for January 2027.
POST /v3/routing/solve, reads the response, and then reuses the same data for two follow-ups: scoring a manual change with /v3/routing/evaluate and placing a new job with /v3/routing/suggest. The solve and evaluate responses are real output for the requests shown. Yours can differ in the travel figures, if the road data has changed since, and in timings.
The scenario
All times in the request are local time on 4 November 2026, written with the+01:00 offset.
How each requirement maps to the request:
- Time windows.
time_windowson each job. They are hard: a job that cannot be reached inside its window is left unassigned. - Skills.
required_skillson a job lists what it requires,skillson a vehicle lists what the technician provides. A job goes only to a vehicle that provides every skill it names. See Skills. - Boilers.
delivery_load: { "boilers": 1 }on the two install jobs is stock loaded at the workshop and dropped at the job. It counts against the van’scapacity: { "boilers": 1 }, so the two installs cannot share a van. See Capacity. - Same technician. One
same_vehiclerelation names the two Deinze visits. See Relations. - Lunch. A
windowedbreak of 1,800 seconds on each shift, with one window that bounds when the break may start. See Driver breaks. - Optional visit.
drop_cost: 30on the Bruges check. It is served only when serving it costs less than 30. See Priority and optional tasks. - Workshop. One
depots[]entry, referenced by id from each shift’sstartandend. - Cost.
objective.costsprices a technician’s time at 40 per hour of route and driving at 0.30 per kilometer, labeledEUR.
Solve the day
options.time_limit_s gives the search two seconds. Send the body to POST /v3/routing/validate/solve first to check it without solving.
Read the response
The response below is the real output for that request, with three things trimmed: each stop’ssnapped_location (the coordinate snapped to the road network), and the top-level search_progress and warnings, to keep it short. Response times are in each shift’s own offset, +01:00 here, so they read as local time.
Summary
tasksreadsserved: 7, unassigned: 1: seven of the eight jobs are on a route and one is inunassigned[]. The call still returns HTTP200.vehicles_usedis2.routes[]has an entry forannaand one forbram.chloeserves nothing, and a vehicle shift with no stops gets noroutes[]entry. This request puts no price on using a vehicle; see Objective function for how to add one.distance_mandtime.travel_sare sums over the routes.time.travel_sis driving time only: 8,756 s + 4,064 s = 12,820 s.
Routes and stops
Each route names itsvehicle and shift by the ids you sent, and lists its stops in visiting order between a start and an end stop. Those two stops carry depot: "workshop" because the shift references the depot by id.
Anna’s day, in local time with the seconds dropped:
What to take from it:
- Skills. Every
gasjob is onannaorbram, and bothelectricaljobs are onanna, who has both skills. - Capacity. Each van carries one boiler, so the two installs are on different routes.
load_peakis the highest load on the route:{ "boilers": 1 }on both. - The relation.
boiler-install-deinzeandboiler-commission-deinzeare both onanna, as thesame_vehiclerelation requires. - Time windows.
arrivalis when the vehicle reaches the stop andservice_startis when the work begins. Bram reaches Eeklo at 08:29, before its window opens, so the stop haswait_s: 1805andservice_start09:00. - Breaks. A
breakstop has nolocation, and carries anidonly when the request break has one. Itsarrivalanddepartureare the start and end of the break, andservice_sis its length. It is listed directly before the stop it falls inside, and each break starts after the vehicle has reached that stop, so a break’sarrivalcan be later than thearrivalof the stop listed after it, within that stop’sarrivaltodeparture. Anna reaches Wetteren at 11:55 and takes her break from 12:00 to 12:30 before the two-hour job, so itsservice_startis 12:30 and the stop’sdepartureminusarrivalis itsservice_splus the break pluswait_s: 264, the idle before the break window opens. That idle counts intime.wait_sbut not in thewaitingcost component; it is paid through the span. Bram has no job left when his break window opens at 12:00. A break whose window opens before the shift’stois still taken on a route that is in use, so his lands at the end of the route: theendstop hasarrival11:37,wait_s1,331 (the idle until 12:00) anddeparture12:30. - Durations.
time.travel_sis driving time only.time.span_sis the route’s span from start to finish, including service, waiting and the break: 31,520 s foranna(08:00 to 16:45) and 16,200 s forbram(08:00 to 12:30). The parts add up to it: foranna, 8,756 s travel + 20,700 s service + 264 s waiting + 1,800 s break = 31,520 s. plan. The same routes in the shape a request accepts: task stops only, without start, end and break stops. The two follow-ups below send it back.
The unassigned job
category first. priced_out means the job fits, it has a drop_cost, and serving it would cost at least that fee, so the solver paid the fee instead. The 30 shows up in score.components.drop_costs, and score.unassigned_priority stays 0 because the job was not must-serve.
A priced_out job has one reason and no constraint: no rule kept it out. Branch on category: a longer time_limit_s would leave this job out again. relaxations is empty for this job. Errors lists every category and reason code.
Score
score.cost is the sum of score.components, in the currency you labeled:
Every component is always present, so the ones this request does not use are
0.0. violation_count is 0 on a solve response without pinned plan stops. See Objective function for each component.
Score a manual change with evaluate
A dispatcher decides the Bruges check has to happen today and gives it tobram, who is free after lunch. Before committing that, send the edited plan to /v3/routing/evaluate. The problem is unchanged. The plan is the solve response’s plan with one stop appended to Bram’s route:
problem and plan and no options.
Complete evaluate request
Complete evaluate request
summary, Bram’s route and unassigned. Anna’s route comes back exactly as in the solve response, and plan echoes the plan you sent. snapped_location is removed as before.
score.violation_countis0: the edited plan breaks no hard rule. A broken rule is counted there and listed as aviolations[]entry on the stop or route it concerns.timingshas nosearch_ms, because nothing is searched.- The new stop. Bram reaches Bruges at 12:08 local time and takes his lunch break there, from 12:08 to 12:38. The job’s window opens at 15:00, so
service_startis 15:00 andwait_sis 8,518 s (the waiting that is left after the 30-minute break), and he leaves at 15:45. His route now ends at 16:22 instead of 12:30. unassignedis empty: every job ofproblemis in the plan.
cost rises from 626.768 to 782.51311, which is 155.74511 more:
Serving the visit this way costs 185.74511 against a fee of 30, which is why the solve left it out.
If the visit must always be planned, remove
drop_cost from the job, or raise the fee to what a missed visit costs you. See Evaluate for the violation codes.
Place a new job with suggest
At 11:00 a customer in Destelbergen reports a leaking radiator and can be visited between 13:00 and 17:00. The routes are already under way, so ask where the new job fits without rearranging them. A suggest request takes the same body as evaluate:problem: the same problem with the new job added tojobs.plan: the plan you are keeping. Here it is the solve response’splan, unchanged.
problem that is not in plan is a task to place; there is no list of ids to send.
The job to add:
Complete suggest request
Complete suggest request
suggestions[] entry per task outside plan, in problem order. The entry for radiator-leak-destelbergen has up to three insertions, cheapest first. Each insertion carries a delta (the change in cost, in your currency, and in distance_m and driving travel_s), one placements[] entry per stop it adds (the plan stop’s type and id, the vehicle and shift of the route it goes into, and its arrival, service_start and latest_arrival), and routes[]: every plan route it changes, whole, with the new stop in. The job requires gas, so no insertion names chloe. annual-check-brugge is still in problem and not in plan, so it is a task to place too and gets its own entry, listed first. To get suggestions for the new job only, drop annual-check-brugge from problem (and from any relation that names it).
Suggest changes nothing. To act on an insertion:
1
Swap in the route
Replace the
plan.routes[] entry with the same vehicle and shift as each of the insertion’s routes by that route.2
Score the result
Send the edited plan to
/v3/routing/evaluate, as in the previous section, to get the full timeline and cost.locked_count on a route in plan: no insertion lands before the locked stops. This page does not show a suggest response; Suggest has an annotated one and the full list of insertion fields.
Next steps
The V3 API model
Every request and response field.
Constraints
The rules used here, and the ones this example leaves out.
Evaluate
The plan shape and every violation code.
Suggest
Reading and applying insertions.