V3 is in beta. General availability (GA) is planned for January 2027.
Overview
POST /v3/routing/suggest answers one question: given the plan I already have, where can this new task go, and what does each insertion cost? You send the same body as /v3/routing/evaluate: the problem and the plan you have committed to. Every task of the problem that is not in the plan is a task to place, and for each one you get back a ranked list of feasible insertion positions, best first.
Use it when a new order arrives during the day and a dispatcher (or your own code) should pick a slot, rather than letting the solver rearrange routes that are already on the road. The committed plan is never changed: the API holds no state, so nothing happens until you apply an insertion yourself.
The request
The body has two required parts,problem and plan, plus optional options:
The same
problem object as on /v3/routing/solve: jobs, shipments, vehicles, depots, relations, objective, travel. It must contain the tasks already in the plan and the tasks you want to place: every task not in plan is placed.The committed plan, in the shape every solve and evaluate response returns as
plan: one routes[] entry per vehicle shift, each with vehicle, shift and an ordered stops[] list. Leave the tasks to place out of it; at least one task of problem must be missing from it. A route’s locked_count marks stops that are already dispatched: no insertion lands before them.Maximum number of ranked
insertions in each suggestions[] entry: an integer from 1 to 10. It caps the insertions per entry, not the number of entries. 0 is rejected with 400 (out_of_range) and a value above 10 with 400 (limit_exceeded); neither is clamped. It also sets how widely a task with several stops is searched: each stop after the first keeps at most max(8, 4 × max_insertions) partial placements. A crew is searched the same way whatever its value: every start slot of each member, with every slot of the other member when two members are outside plan, and a few slots of each other member when more are.Search budget in seconds, at most
60; fractions are allowed (2.5). Defaults to 1. A value below 0.1 is raised to 100 ms. The budget is split across the tasks to place in problem order, each getting an equal share of the time left. The clock starts when the search starts, so fetching travel times does not eat into it.What to do when travel times cannot be fetched, as on
/v3/routing/solve. estimate ranks the insertions on travel estimated from straight-line distance and a typical speed for the vehicle profile, and sets travel_estimated to true. fail returns the 502 or 503 instead. Coordinates that span more than one region are a 400 either way.plan or be left out of problem. There is no options.tasks field: sending one is unknown_field.
Example
One truck already servesdelivery-1 and delivery-2. A third order, delivery-3, comes in and must be served between 08:00 and 11:00 local time.
The response
delivery-3 after delivery-2 would reach it after its window closes, so that position is not feasible and is not listed.
suggestions[] has one entry per task outside plan, in the order the tasks appear in problem (jobs first, then shipments). Tasks tied together by a relation share one entry: see Shipments and related tasks.
Present only when
insertions is empty. code is time_limit, candidates_exhausted or infeasible (new codes may be added; handle one you do not know as not final), and message says the same as an English sentence, for people and logs. See When a task gets no insertions.One flag for the whole response.
true when options.time_limit_s cut the search short for any task, when an empty list for a multi-task unit is inconclusive, or when a crew with three or more members outside plan lists fewer than max_insertions after it was refused where it had left slots untried. The lists may then be incomplete or miss the best insertion.true when travel times could not be fetched and the insertions were ranked on estimated travel, as summary.travel_estimated on a solve. Every delta, arrival and feasibility verdict then holds only against the estimate, which can be tens of percent off. Absent when travel came from the road network. Send options.travel_fallback: "fail" to get an error instead.When a task gets no insertions
An emptyinsertions list is a 200, not an error. The entry’s reason says why: code to branch on, message in a sentence:
In code, branch on
reason.code, never on reason.message. Only infeasible is final; the other two can change with a larger budget or search. New codes may be added: treat one you do not know as not final and show reason.message. truncated covers the whole response: a task whose list is not empty can also have been cut short, and carries no reason. Send one task per request when you need to know which task was cut short.
Reading an insertion
An insertion says what it changes (delta), which stops it adds (placements) and how every changed route reads with it in (routes).
Insertions are ordered by
delta.cost, lowest first.
Each placement says who serves the new stop and when, which is what a booking screen shows:
delta.cost and delta.travel_s measure different things. In the example the first insertion adds more driving than the second, yet costs less: the truck used to wait at delivery-1 for the window to open, and the detour fits inside that wait, so the route does not end any later. See Objective function for what the cost is made of.Applying a suggestion
Suggest only reports insertions. To act on one, swap its routes into your plan:1
Swap in the routes
For each entry of the insertion’s
routes, replace the plan.routes[] entry with the same vehicle and shift, or add it when plan has none. To use one of interchangeable_vehicles instead, first put its vehicle and shift in the insertion’s route. For the first insertion above, plan becomes:2
Confirm or re-optimise
Send the edited plan to
/v3/routing/evaluate to get the full timeline and cost of the new plan, or to /v3/routing/solve as a warm start if the solver may also rearrange the other stops.Shipments and related tasks
- Shipments. Leave both legs of the shipment out of
plan. Its pickup and delivery are placed together, on one route: the insertion’splacements[]lists both legs in route order, each with itstypeofpickupordeliveryand its leg’s stop id. A shipment with only one leg inplanis rejected by the plan import, as on evaluate. sequence,same_vehicle,same_dayandsame_routerelations. Tasks tied together by one of these relations are placed as one unit and get one entry, named after the first new task of the unit. Only the tasks outsideplanform the unit; a member already inplanconstrains where the others can go.synchronizedandtime_lagrelations. The tasks one of these ties together are placed as one unit: see Crews.- Reloads. An insertion can land in any existing trip of a multi-trip route, but suggest never adds a new reload trip.
Crews: synchronized and time_lag
A job that needs several technicians is one job per technician in a synchronized relation (see Constraints). Leave every member out of plan and suggest finds the times when the whole crew is available:
- The crew gets one
suggestions[]entry, named after the member that comes first injobs. Each insertion places every member:placements[]has one entry per member androutes[]every route they go into, so applying it books the whole crew. - With the default
on: service_start, the members start withinmax_spread_sof one another (withon: departurethey finish within it), so with a spread shorter than the service each is on a different vehicle. Read the booking time fromservice_start: a member who arrives earlier waits for the others, shown ashold_son the stop when you/evaluatethe applied plan. - One
delta.costcovers the whole crew, and insertions are ranked by it across every route and shift inplan, so a cheaper day comes first. - Two insertions that differ only in which member goes to which of the same vehicles, at the same times and cost, are listed once when the swapped members share a location and service time.
- A member already in
plankeeps its route and position, and the others are placed to meet it. It may be held later to meet them, which delays the stops after it on its route:/v3/routing/evaluateof the applied plan shows the wait ashold_s, anddelta.costincludes it.routes[]lists only the routes whose stops change, so that route is not in it. - Tasks a
time_lagties together are placed the same way, with the lag kept.
eligible_vehicles restricts. With two members outside plan, every slot of the other member is tried against each start, so the cheapest pairing is listed even when the other member’s cheapest slot would make the crew wait. With more, each other member, those fewer vehicles can serve first, is placed at its two cheapest slots and at the slot arriving nearest the first member’s start, so a different combination can cost slightly less than the one listed. A plan whose committed crews already break their own synchronized or time_lag relation still gets insertions for a new crew: each one keeps that violation and adds none, as /v3/routing/evaluate of the applied plan shows.
One case is not supported: a crew member that a sequence, consecutive, same_vehicle, same_day, same_route or all_or_none relation also ties to a task outside the crew, such as an all_or_none over two crews. It makes the whole request a 400, item not_supported, pointing at the member. Put the other task, or the crew, in plan.
Errors
Suggest returns the same error body as the other endpoints. The cases specific to it:POST /v3/routing/validate/suggest checks problem, options, and that plan leaves at least one task out, without fetching travel times or searching. It does not check that plan fits the problem; only the real call does.
Evaluate
Score the plan after you apply an insertion.
Errors
The error body, every code, and what to do about each.
The V3 API model
The
problem and plan objects in full.API reference
Request and response schema for every endpoint.