V3 is in beta. General availability (GA) is planned for January 2027.
Overview
A plan built in the morning is out of date by the first coffee break. The V3 Routing API keeps no state, so a repair is a new request that carries the current plan with it: you editproblem to describe what changed, send the
routes you want kept as plan, and pick the endpoint that fits the size of
the change.
Three facts about
plan shape every pattern below. The response’s plan is
the same object a request takes, so it goes back in unchanged. A plan that
names a task, vehicle, shift or depot that is not in problem is a 400
pointing at /plan/routes/{i} or /plan/routes/{i}/stops/{k}, so every
edit to problem has a matching edit to plan. And a response’s plan
never carries locked_count or pinned: you set them again on every call.
Disruption, field, call
Every row sends the whole
problem, not a delta: the API has no memory of
the previous request.
What is hard and what is a hint
A task in a plan can be tied to a vehicle or not, and to a position or not. The four cases use different fields, and only three are hard rules.
What each means when a hard rule is broken:
- An unlocked stop (the hint) that violates capacity, skills, eligibility,
a limit or a hard time window is taken out of its position, reinserted
where it fits, or returned in
unassigned[]with a reason. - A locked or pinned stop that violates a hard rule can be neither
moved nor dropped, so solve returns
400, iteminvalid_value, pointer/plan, naming the stop, the vehicle and the rule. On an async run the run endsfailedinstead. Send the same body toevaluateto see every broken rule stop by stop before you decide what to relax. pinnedis accepted and ignored byevaluateandsuggest.locked_countis inert onevaluate; onsuggest, no insertion lands before the locked stops.- A pinned or locked stop switches off
all_or_nonefor its whole group, and a pinned stop that shares asame_vehicle,same_dayorsame_routegroup with another pinned or locked stop the relation forbids is a400.
locked_count for what the driver has done or has been
told, pinned for what the customer has been promised a vehicle for, nothing
for everything else.
What a warm start reuses, and when a solve is cold
Aplan on solve seeds the search instead of constructing from scratch, so
the budget goes into improving a plan that is already good. Stops move
wherever the search finds a better place, except the ones you lock or pin.
The first search_progress entry is your plan’s score, so you can read how
much the repair changed it.
Three consequences for a repair:
- It ends early. A warm-started solve stops once it has gone 5% of the
budget, and at least 150 ms, without finding a better plan, and reports
summary.search.termination: "no_improvement". The repair in the example below was sent withtime_limit_s: 1and returned after 150 ms. - The default budget is enough to start with. Omitted,
time_limit_sis 150 ms plus 4 ms per task. Raise it for the morning solve after measuringsummary.scoreat a few budgets on your own data, up to60on solve and3600on a run; leave the repair at the default or the same budget, and let the early stop return it sooner. A value below0.1is raised to the 100 ms floor, so a smaller number buys nothing. - Travel data is reused when it can be. Without
travel.departure_time, a request on theosmortomtomengine with at most 200 distinct locations can reuse travel data looked up within the last hour, so a repair that keeps the same locations skips most of the lookup.departure_timefetches fresh data on every call.
plan, when the plan is rejected with a
400 (an id that is not in problem, a stop listed twice, a locked or
pinned stop that cannot be driven), and, in effect, when so many stops are
unlocked and infeasible that the search rebuilds most routes. Keep the plan
valid against the edited problem and the warm start holds.
options.seed defaults to 42, and the same seed on the same request
usually returns the same plan, though the search is bounded by time rather
than by work, so it is not guaranteed. Keep one seed across a day’s repairs
so a repeated call does not reshuffle routes for no reason; vary it only
when you deliberately explore alternatives.
One insertion: suggest
When one order arrives and the routes are already on the road, a full solve
is the wrong tool: it may move every unlocked stop to save a few minutes, and
a dispatcher then has to re-brief every driver. suggest leaves the plan as
it is and ranks the feasible positions for each task of problem that is not
in plan, lowest delta.cost first, within options.time_limit_s (default
1) and up to options.max_insertions per task (default 5, at most 10).
Use it when the dispatcher chooses, when the customer is on the phone and
needs an arrival time now (placements[].service_start), or when a change
must not ripple through other routes. Use solve with plan when the task
gets an empty insertions list with reason.code: "infeasible": no position
in the committed plan works, but the solver may make room by moving unlocked
stops.
To apply a suggestion, swap in its routes: for each entry, replace the
plan.routes[] entry with the same vehicle and shift, or add it when the
plan has none, then send the edited plan to evaluate for the full timeline
or to solve as a warm start. Each task is ranked against the same committed
plan, so after applying one insertion, call suggest again for the next task.
See Applying a suggestion.
Call frequency
- Batch disruptions. One repair that carries a cancellation, a late driver and two new orders is cheaper and more stable than four repairs in a row, each moving stops the previous one placed. Collect changes, then send one request per plan.
- One repair in flight per plan. Two overlapping warm starts from the same plan return two plans that disagree. Wait for the response, apply it, then send the next batch from the plan it returned.
- Poll a run’s status first. On an async run, read
GET /v3/routing/runs/{id}untilstatusissucceededbefore fetching/result; before then/resultis a409withrun_not_finished. Cancel a run that a newer batch has made obsolete withPOST /v3/routing/runs/{id}/cancel. - Respect the run limit. At most 10 runs per account may be
queuedorrunning; one more is a429withRetry-After: 30. A cancelled run keeps counting until its worker stops. - Do not shrink the time limit to go faster. Below
0.1the budget is raised to 100 ms anyway, and a warm start already returns early on its own. Shorten the wall-clock instead by leaving outoptions.polylineandtravel.departure_timeon repairs. - Back off on
429and503. A429from the gateway is the rate limit or the ten-run cap, and its body is not the error body; a503with codeoverloadedmeans no solver is free and carries aRetry-Afterheader. Retry both with backoff, and spread out bursts such as a top-of-the-hour re-solve of every route.
Worked example
One van, three jobs in Ghent, straight-line travel so the figures are reproducible. The shift runs 08:00 to 17:00;boiler-gent has a morning
window and radiator-ledeberg an afternoon one. Both requests below were sent
to https://api.solvice.io/v3/routing/solve and the responses are trimmed to
summary, plan and unassigned.
Morning solve
thermostat-wondelgem at 08:02,
boiler-gent at 08:26 (waiting for its 09:00 window) and radiator-ledeberg
at 09:32 (waiting until 13:00), back at base at 13:48:
search_ms is 500 although the budget was 2 seconds: a one-vehicle solve
with time windows and fewer than 60 visits is capped at 500 ms. See
When a solve returns before the budget.
Repair at 09:40
The driver has finished the first two stops.radiator-ledeberg cancels, and
a leak in Sint-Amandsberg must be fixed between 10:00 and 12:00. The repair
request differs from the morning one in four places:
radiator-ledebergis gone fromjobsand fromplan.routes[0].stops.leak-sint-amandsbergis added tojobs, with"priority": 5, and is not inplan.- The morning response’s
planis the requestplan, with"locked_count": 2so the two completed stops stay where they are. time_limit_sdrops to1; the warm start will return sooner anyway.
termination is no_improvement after 150 ms:
the warm start found nothing better than the plan it was given plus the
inserted leak, and stopped. search_progress (not shown) opens at
unassigned_priority: 5, the leak outside the plan, and reaches 0 in the
first improvement. And the returned plan has no locked_count: the next
repair sets it again, this time to 3.
Sent as a suggest body instead (same problem and plan, options of
{ "max_insertions": 3 }), the leak gets one insertion, after boiler-gent,
with delta.cost of 33.97688: the 88.1696 above minus the 54.19272
the two-stop plan costs on its own. The other two positions are inside the
locked prefix and are not offered.
Next steps
Warm-starting with plan
The
plan object, locked_count and pinned in full.Suggest
Ranked insertions for a new task, and how to apply one.
Async runs
Repairs of more than 400 tasks, run limits and polling.
Performance guide
Choosing
time_limit_s, and why a warm start returns early.