V3 is in beta. General availability (GA) is planned for January 2027.
Overview
V2 (OnRoute) and V3 describe the same routing problem in different vocabularies. This page maps one to the other: endpoints, request fields, relations, the objective, and the response. Solvice maintains a V2-to-V3 compatibility mapping: a translation of a V2 request body into a V3 one. The tables below follow that mapping, so the V3 column shows what a V2 field means in V3 terms. The mapping does not cover every V2 field: V2 fields that are not carried over lists the ones it leaves out and what to send natively instead.Every statement on this page about “the compatibility mapping” describes the
translation of a V2 request into a V3 one: what the translated request keeps and
what it loses. The page makes no statement about how a call to a V2 endpoint is
processed. Use those statements to decide what to write in the native V3
request.
How to port a request
1
List what does not carry over
Check every field your V2 request sends against
V2 fields that are not carried over.
A field in that list has no automatic translation, so decide its V3
replacement before you start.
2
Rewrite the body
Wrap the problem in
problem, then rename fields with the
Conventions, Vehicles,
Shifts and Jobs tables. Give every shift an id.3
Convert relations and pickup-delivery pairs
Rewrite
relations[] with the Relations table, and turn each
PICKUP_AND_DELIVERY pair into a
shipments[] entry.4
Set the objective explicitly
A V3 request without
objective prices time and distance at default rates.
Read Default objective, then convert the weights you
rely on with From weights to prices.5
Move the warm start and the options
Replace
initialResource and initialArrival with a request-level plan
(see Existing plans), and move the time budget and the
other options with the Options table.6
Validate without solving
Send the request with
?validate_only=true. A valid request returns
{"valid": true, "warnings": []}. An invalid one returns the 400 the real
call would return: fix each errors[] item at its pointer. A leftover V2
key shows up as unknown_field. See Errors.7
Solve and compare
Solve the same instance and read the result with the
Response mapping tables. Compare
unassigned[], the
route totals and the stop order with your V2 result before you switch
traffic.Endpoints and lifecycle
Only solve has a background form. The three synchronous V3 endpoints hold the
connection open until the result is ready: a solve searches for
options.time_limit_s seconds (at most 300), and fetching travel times comes
on top of that. Set your HTTP client timeout above the budget you send, or
submit a run, which searches for up to 3600 seconds
without holding a connection. See Limits.
What each V2 step gave you is available like this:
- Solution. The body of the
solveresponse, or/resultof a run. - Explanation. Every entry of
unassigned[]carries acategory, machine-readablereasons[]and best-effortrelaxations[]. For a plan you built or edited yourself,evaluatereportsviolations[]per stop and per route. - Change. Keep the
planfrom the response, edit it, and send it back: toevaluateto score the edit, or tosolveas a warm start.locked_counton a plan route freezes its first stops.
Existing plans
Solve (as a warm start), evaluate and suggest all take an existing plan, and its shape changes. V2 reads it fromjobs[].initialResource and
jobs[].initialArrival; V3 takes an explicit plan object next to problem,
which is exactly what a V3 response returns under plan. Each plan.routes[]
entry names its vehicle and its shift, and lists its stops in order. On
suggest, V2 places the jobs that have no initialResource; V3 places every
task of problem that is not in plan.
initialResource and initialArrival is narrower than a native plan:
- First shift only. Every job with an
initialResourceis placed on that resource’s first shift, ordered byinitialArrival. On a solve, a plan that spans several days is therefore folded onto the first day without an error. On evaluate and suggest, a resource that has more than one shift and at least one job withinitialResourceis rejected with400. Natively,plan.routes[].shiftnames the shift of each route. - Order only.
initialArrivaldecides the order of the stops. The arrival time itself is not carried over: V3 recomputes every time from the stop order. A job without aninitialArrivalsorts after the timed ones, by its position in the firstSEQUENCErelation that lists it. - Nothing is frozen. The mapping sets no
locked_count. Natively, setlocked_counton a plan route to freeze its first stops. - Skipped entries. On a solve, an
initialResourcethat names no resource in the request is left out, and so is one that contradicts aplannedResourceor a relation’sresourceon the same job. On evaluate and suggest each of those is a400. So is a job that has aninitialResourcebut noinitialArrival, when it is in noSEQUENCErelation, and so is a resource whose committed jobs mix timed and untimed jobs, or take their position from differentSEQUENCErelations. Order a resource’s committed jobs either byinitialArrivalon every job or by oneSEQUENCErelation. - No shipments. When any job in the request carries
initialResource, noPICKUP_AND_DELIVERYpair is turned into a shipment. On suggest, a request that combinesinitialResourcewith a pair still to be placed is rejected with400. See Pickup and delivery pairs. initialDepotis not read. Natively, a reload is a{ "type": "reload", "depot": "<depot id>" }stop in the plan route.
Conventions
Request mapping
Vehicles (resources)
Per-vehicle limits live under
limits: limits.max_distance_m takes the place
of V2 maxDriveDistance, and limits.max_route_duration_s caps the whole route
span (travel, service and waiting), not driving time alone.
limits.max_leg_travel_time_s takes the place of maxDriveTimeJob: a cap on
any single travel leg. V3 has no cap on a route’s total driving time, so
maxDriveTimeInSeconds and maxDriveTime have no counterpart.
Shifts
Multi-day planning works the same way in both versions: one shift per working
day. In V3 each shift has its own
id, and each route reports the shift it
runs on.
Breaks
deductPreviousBreaks has no V3 counterpart: model each accumulation window
as its own break.
Depots (multi-trip reloads)
depots[]is top-level in both versions. In V2 every shift may use every depot; in V3 a shift opts in by naming depot ids in its ownreloads[]. A shift withoutreloadsis single-trip.namebecomesid, anddurationbecomesservice_duration_s. Both keep one handling time per depot, and the compatibility mapping carries each depot’sdurationover as itsservice_duration_s.costbecomescost_per_visit, charged once per reload. The compatibility mapping does not carry it over.- A depot can also be a shift’s start or end:
"start": { "depot": "hub" }. shifts[].max_tripscaps the number of trips in a shift. V2 has no such cap.
Jobs
Time windows
A V3 window has nohard flag and no weight. It is hard unless lateness is
priced:
- Per window:
time_windows[].per_late_hourmakes that window soft on its late side, at that price per hour. It is allowed only on a task with exactly one window and must be greater than0. This is where a V2hard: falsewindow goes. - Fleet-wide:
objective.per_late_hourprices lateness on every window that does not set its own rate.
from. When a V2 request is translated by the
compatibility mapping, neither windows[].hard nor windows[].weight is read,
so every window the mapping produces is hard.
A job can list several windows and is served in one of them. An optional
time_windows[].cost ranks them when one window is preferred over another; it
cannot be combined with objective.per_late_hour.
Tags
When you port natively, decide per tag:- A requirement becomes
jobs[].skills: [{ "name": "plumbing" }]. The vehicle, or the shift that serves the job, must list every required skill.skills[].min_levelandvehicles[].skills[].leveladd proficiency levels. - A preference (V2
hard: falsewith aweight) becomesjobs[].preferences[]: one entry per vehicle that has the capability, each with the sameviolation_cost. Every listed vehicle that does not serve the job charges its cost, so a vehicle with the capability is cheaper by oneviolation_costthan a vehicle without it.
GROUP_SEQUENCE group is a group label, not a skill. It
becomes jobs[].tags.
Priority and optional jobs
V3 has two separate mechanisms. Pick by what should happen when not every job fits.
V2
priority maps to V3 priority. The V2 reference weighs an unserved job
by priority × duration, and when a V2 request is translated by the
compatibility mapping, exactly that product (duration in seconds) is sent as
the V3 priority. Do the same to keep V2’s ranking, or
send your own rank if job length should not count.
priority and drop_fee cannot be combined on one job (400).
A job with neither field is must-serve.
Planned arrival and urgency
A V3target is a point in time with a price for missing it on the late side:
V3 target
per_late_houris required and must be greater than0. It is charged per hour of arrival afterat, prorated per second.- Arriving before
atcosts nothing, and the vehicle does not wait forat. V2 documentsplannedArrivalas “as close as possible” to the planned time; a V3targetdoes not price the early side. Add atime_windowsentry when the job must not start before a given time. "on": "end"measures the end of service instead of the arrival.
target is
produced only for a job that sends both plannedArrival and an urgency
greater than 0:
The mapping’s price is on its own scale, in which route time costs 0.036 per
hour. See From weights to prices before you reuse the
value next to a different hourly rate.
Relations
V3 relations are a tagged union underproblem.relations[], with members in
stops instead of jobs. All V3 relations are hard: there is no relation
weight. Every V2 relation type is listed here.
resource pins every member to that vehicle. V3 relations have
no such field: set locked_vehicle on each job instead.
Relation fields that change meaning:
-
SAME_TIMEtolerance. The V2 reference documentsmaxWaitingTime(default 1200 seconds). When a V2 request is translated by the compatibility mapping,maxTimeIntervalis read instead, and without it the tolerance is0seconds. Natively,max_wait_sis required. -
SAME_TIMEwithminTimeIntervalon three or more jobs is rejected by the compatibility mapping (400). Write onetime_lagper pair natively. -
DIRECT_SEQUENCEtime intervals. V2 appliesminTimeIntervalandmaxTimeIntervaltoDIRECT_SEQUENCEas well as toSEQUENCE. The compatibility mapping carries them over forSEQUENCEonly: on aDIRECT_SEQUENCErelation both bounds are left out without an error. Natively, add onetime_lagper consecutive pair next to thesequenceandsame_vehiclerelations:The lag is measured between the two service starts. AddV3 relations for a DIRECT_SEQUENCE with a time interval"on": "end"tofromfor V2timeInterval: FROM_DEPARTURE. -
hardMinWaitandweighthave no V3 counterpart: a time bound cannot be softened and a relation cannot be weighted. -
partialPlanning.same_vehicle,same_dayandtime_lagbind the members that are served: a member that ends up inunassigned[]imposes nothing on the others, andsequenceonly orders the members that share a route. That is the V2partialPlanning: truebehaviour. V2 defaults topartialPlanning: false, so aSAME_TIMErelation of two or more jobs that does not set it totrueis all-or-nothing, and the V2 endpoints add anall_or_nonerelation for it. To keep that in V3, add anall_or_nonerelation over the samestopsnext to thesynchronizedone.all_or_noneonly takes jobs that asynchronizedortime_lagrelation also names, so for the other relation types checkunassigned[]for members of the relation after each solve. A shipment is different: it is one task, so its pickup and delivery are served as a pair or the shipment is listed once inunassigned[].
Pickup and delivery pairs
A V2 pickup-and-delivery pair is two jobs plus a relation. In V3 it is oneshipments[] entry and no relation.
- On the leg (
pickupordelivery):location,setup_duration_s,service_duration_sandtime_windows. A legidis optional; set it to the V2 job name to keep that name on the stop in the response. - Once on the shipment:
load(the load carried from pickup to delivery),priority,drop_fee,skills,eligible_vehicles,locked_vehicle,preferencesandservice_factors. They apply to the pair. - The shipment
idmust be unique acrossjobsandshipments.
pickup,priority,service_factors(fromproficiency) andeligible_vehicles(fromallowedResourcesanddisallowedResources) are taken from the pickup job. The delivery job’sload,priority,proficiencyand resource lists are not carried over.- Tags with
"hard": trueon either job becomeskillsof the shipment. locked_vehiclecomes from the relation’sresource. AplannedResourceon either job is not carried over.rankingson either job are not carried over.
GROUP_SEQUENCE tag or a plannedArrival with a positive urgency,
and no job in the request carries initialResource. Any other pair stays two
jobs with a sequence and a same_vehicle relation.
Period rules
V2resources[].rules[] splits across two V3 mechanisms.
limits.periods[] caps a vehicle’s totals per "every": "day", per
"every": "week", or per explicit from and to window, with any of
max_duration_s, max_distance_m and max_tasks. A shipment counts as two
stops toward max_tasks. See Constraints.
When a V2 request is translated by the compatibility mapping, only
jobTypeLimitations (with period and groupTag) is enforced. The work,
service, drive and complexity limits are not.
Options
New in V3:
options.seed for repeatable runs, and problem.travel.departure_time
to plan against the traffic of a given instant (not with osm).
V3 has no caller-supplied distance matrix. The API computes travel times and
distances itself, selected by travel.engine and vehicles[].profile.
Default objective
A V3 request that sends noobjective is not optimized for the same thing as a
V2 request translated by the compatibility mapping. The mapping always sets an
explicit objective; a native request without one gets the V3 defaults. Amounts
are in the objective’s currency.
A request ported field for field therefore also starts paying for distance,
which can change which routes win. To keep the mapping’s time-only objective,
set the distance rate to zero. Rates you leave out keep their defaults:
minimizeResources: true, the compatibility mapping charges each used
vehicle the equivalent of one hour of route time. Natively, set
objective.costs.fixed to your hourly rate:
From weights to prices
V3 has no weights. Every objective term is a price in the objective’s currency: a fleet rate inobjective.costs, or a price on the field itself (drop_fee,
preferences[].violation_cost, target.per_late_hour,
time_windows[].per_late_hour). No V2 weight is converted for you when you
write a request natively, so choose each price yourself: a price is the amount
of route cost you accept to avoid the thing it prices.
A price only has meaning next to the hourly rate it competes with. When a V2
request is translated by the compatibility mapping, route time is priced at
0.036 per hour, which is 0.00001 per second, and the mapping derives a few
other prices on that same scale:
For
rankings[], rankingWeight counts as 1 when the request sends no
weights object and as 0 when weights is sent without it;
travelTimeWeight counts as 1 when it is absent or not positive; and
ranking is limited to the range 1 to 100. The price is charged for the
resource that serves the job, as in V2. A resource the job does not list counts
as rank 100; the mapping applies that default itself, so it is not written
into the request. A job whose listed resources all have the same ranking is
still charged, because the absolute price is what makes a job ranked 100
everywhere the first one dropped under capacity pressure. Nothing is charged
when the job has no rankings, when rankingWeight is 0, or when the rate
is so small that the rank-100 price rounds to zero. All other V2 weights are
not read.
These values keep their meaning only next to per_route_hour: 0.036. To keep
the same trade-off at another hourly rate, multiply each of them by
per_route_hour / 0.036. At the V3 default of 35 per hour that factor is about
972: an urgency of 100 is 0.001 per late hour on the mapping’s scale and
0.97 per late hour next to a rate of 35.
objective.grouping.cost_per_reentry follows the same rule: at 35 per hour the
rescaled default of 0.003 is about 2.9. It accepts at most 1000, and a
larger value is a 400.
Response mapping
objective.
Routes and stops
stops[].type takes seven values, and a client must handle all of them:
A
break stop appears for each break a route takes, and a reload stop for
each mid-shift return to a depot. V2 has no reload visit: a V2-shaped response
leaves those stops out.
Summary, score and unassigned jobs
New in V3:
summary.tasks.served, tasks.unserved, vehicles_used,
timings.total_ms, and a top-level plan that mirrors routes in the
request’s vocabulary. Send that plan back unchanged to evaluate, to
suggest, or to solve as a warm start.
unassigned[].category says what kind of miss it is: impossible (no vehicle
can ever serve it), blocked (it does not fit alongside the jobs that were
served), priced_out (its drop_fee was cheaper than serving it) or
search_limit (a feasible position exists but was not found in the time
budget).
Suggest response
V2 returns onesuggestions[] entry per alternative, each with a score and
its assignments[]. V3 returns one suggestions[] entry per task outside
plan, with that task’s alternatives as ranked insertions[], best
first. Tasks linked by a relation are placed together and share one entry.
New in V3:
suggestions[].reason_code and reason when insertions is empty,
insertions[].interchangeable_vehicles, and the top-level truncated and
elapsed_ms. See Suggest for every field.
What a V2-shaped response contains
When a V2 request is translated by the compatibility mapping and solved, the V2-shaped response that comes back is filled as follows.- Populated:
status(alwaysSOLVED),score,messages,unserved,unservedReasons, the four totals, and per tripresource,date,departureTime,travelTime,distance,serviceTime,waitTime,workTime,start,endandvisits[]. Every visit carriesjob,arrival,serviceTime,travelTime,distance,waitTime,breakTime,activity,location,snappedLocationandlatlon. score:hardScoreandmediumScoreare0andfeasibleistrueon a solve, because a job that cannot be served is returned inunservedinstead of producing an infeasible plan.softScoreis the negated sum of the time, distance, waiting, overtime and drop-fee cost components, in units of 1/100,000 of the objective’s currency, so compare it only with other scores from the same request. On evaluate,hardScoreis the negated number of violations,feasibleisfalsewhen there are any, andsoftScoreis0.unservedReasons: the values are reason codes such asTIME_WINDOW_VIOLATEDorCAPACITY_EXCEEDED, not sentences.messages: warnings about parts of the request that were accepted but not enforced, for example a rule whose period overlaps no shift.- Suggest: every assignment carries
job,resource,suggestedArrival,latestArrival,executedAfter,suggestedInitialArrival(equal tosuggestedArrival),scoreand an emptyviolations.scoreExplanationis not returned. - Not returned:
id,occupancy,estimatedCost,unresolved,violations,workloadFairness,trips[].polyline,trips[].occupancy, and depot reload visits.
V2 fields that are not carried over
The compatibility mapping does not read the fields below, exceptdeductPreviousBreaks, whose row says where it is honoured. When a V2 request
that sends them is translated by the mapping, they are left out without an
error, so the translated request is solved as if they were absent. The
right-hand column is what to send when you write the request natively.
Top-level fields
Resource and shift fields
Job fields
Relation fields
Depot fields
Option fields
Feature parity
Quickstart
Send your first native V3 request.
The V3 API model
The full request and response model.
Constraints
Every constraint field, with examples.
Errors
The error body and its codes.
Field service day
A worked V3 example: one solve, then evaluate and suggest on the same data.
Suggest
Rank insertion positions for new tasks.