Skip to main content
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.
Three conventions cover most of the work: names become snake_case with a unit suffix (_s seconds, _m metres), positional arrays become named maps, and V2 weights are replaced by prices in the objective’s currency that you set yourself. See From weights to prices.

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 solve response, or /result of a run.
  • Explanation. Every entry of unassigned[] carries a category, machine-readable reasons[] and best-effort relaxations[]. For a plan you built or edited yourself, evaluate reports violations[] per stop and per route.
  • Change. Keep the plan from the response, edit it, and send it back: to evaluate to score the edit, or to solve as a warm start. locked_count on 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 from jobs[].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.
When a V2 request is translated by the compatibility mapping, the plan it builds from initialResource and initialArrival is narrower than a native plan:
  • First shift only. Every job with an initialResource is placed on that resource’s first shift, ordered by initialArrival. 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 with initialResource is rejected with 400. Natively, plan.routes[].shift names the shift of each route.
  • Order only. initialArrival decides the order of the stops. The arrival time itself is not carried over: V3 recomputes every time from the stop order. A job without an initialArrival sorts after the timed ones, by its position in the first SEQUENCE relation that lists it.
  • Nothing is frozen. The mapping sets no locked_count. Natively, set locked_count on a plan route to freeze its first stops.
  • Skipped entries. On a solve, an initialResource that names no resource in the request is left out, and so is one that contradicts a plannedResource or a relation’s resource on the same job. On evaluate and suggest each of those is a 400. So is a job that has an initialResource but no initialArrival, when it is in no SEQUENCE relation, and so is a resource whose committed jobs mix timed and untimed jobs, or take their position from different SEQUENCE relations. Order a resource’s committed jobs either by initialArrival on every job or by one SEQUENCE relation.
  • No shipments. When any job in the request carries initialResource, no PICKUP_AND_DELIVERY pair is turned into a shipment. On suggest, a request that combines initialResource with a pair still to be placed is rejected with 400. See Pickup and delivery pairs.
  • initialDepot is 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 own reloads[]. A shift without reloads is single-trip.
  • name becomes id, and duration becomes service_duration_s. Both keep one handling time per depot, and the compatibility mapping carries each depot’s duration over as its service_duration_s.
  • cost becomes cost_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_trips caps the number of trips in a shift. V2 has no such cap.

Jobs

Time windows

A V3 window has no hard flag and no weight. It is hard unless lateness is priced:
  • Per window: time_windows[].per_late_hour makes 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 than 0. This is where a V2 hard: false window goes.
  • Fleet-wide: objective.per_late_hour prices lateness on every window that does not set its own rate.
Arriving early always waits for 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

The V2 reference documents a job tag without hard as a hard requirement. When a V2 request is translated by the compatibility mapping, a job tag becomes a required skill only when the tag carries an explicit "hard": true. A tag sent as { "name": "plumbing" }, or with "hard": false, is not carried over, and weight is never read: in the translated request the job can be served by a vehicle without that tag. For a request that is to be translated by the mapping, send "hard": true on every tag that must be enforced.
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_level and vehicles[].skills[].level add proficiency levels.
  • A preference (V2 hard: false with a weight) becomes jobs[].preferences[]: one entry per vehicle that has the capability, each with the same violation_cost. Every listed vehicle that does not serve the job charges its cost, so a vehicle with the capability is cheaper by one violation_cost than a vehicle without it.
A tag that names a 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 V3 target is a point in time with a price for missing it on the late side:
V3 target
  • per_late_hour is required and must be greater than 0. It is charged per hour of arrival after at, prorated per second.
  • Arriving before at costs nothing, and the vehicle does not wait for at. V2 documents plannedArrival as “as close as possible” to the planned time; a V3 target does not price the early side. Add a time_windows entry when the job must not start before a given time.
  • "on": "end" measures the end of service instead of the arrival.
When a V2 request is translated by the compatibility mapping, a 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 under problem.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.
A V2 relation’s 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_TIME tolerance. The V2 reference documents maxWaitingTime (default 1200 seconds). When a V2 request is translated by the compatibility mapping, maxTimeInterval is read instead, and without it the tolerance is 0 seconds. Natively, max_wait_s is required.
  • SAME_TIME with minTimeInterval on three or more jobs is rejected by the compatibility mapping (400). Write one time_lag per pair natively.
  • DIRECT_SEQUENCE time intervals. V2 applies minTimeInterval and maxTimeInterval to DIRECT_SEQUENCE as well as to SEQUENCE. The compatibility mapping carries them over for SEQUENCE only: on a DIRECT_SEQUENCE relation both bounds are left out without an error. Natively, add one time_lag per consecutive pair next to the sequence and same_vehicle relations:
    V3 relations for a DIRECT_SEQUENCE with a time interval
    The lag is measured between the two service starts. Add "on": "end" to from for V2 timeInterval: FROM_DEPARTURE.
  • hardMinWait and weight have no V3 counterpart: a time bound cannot be softened and a relation cannot be weighted.
  • partialPlanning. same_vehicle, same_day and time_lag bind the members that are served: a member that ends up in unassigned[] imposes nothing on the others, and sequence only orders the members that share a route. That is the V2 partialPlanning: true behaviour. V2 defaults to partialPlanning: false, so a SAME_TIME relation of two or more jobs that does not set it to true is all-or-nothing, and the V2 endpoints add an all_or_none relation for it. To keep that in V3, add an all_or_none relation over the same stops next to the synchronized one. all_or_none only takes jobs that a synchronized or time_lag relation also names, so for the other relation types check unassigned[] 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 in unassigned[].

Pickup and delivery pairs

A V2 pickup-and-delivery pair is two jobs plus a relation. In V3 it is one shipments[] entry and no relation.
Where each V2 job field goes:
  • On the leg (pickup or delivery): location, setup_duration_s, service_duration_s and time_windows. A leg id is 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, preferences and service_factors. They apply to the pair.
  • The shipment id must be unique across jobs and shipments.
When a V2 request is translated by the compatibility mapping, the two jobs do not contribute equally:
  • pickup, priority, service_factors (from proficiency) and eligible_vehicles (from allowedResources and disallowedResources) are taken from the pickup job. The delivery job’s load, priority, proficiency and resource lists are not carried over.
  • Tags with "hard": true on either job become skills of the shipment.
  • locked_vehicle comes from the relation’s resource. A plannedResource on either job is not carried over.
  • rankings on either job are not carried over.
The mapping turns a pair into a shipment only when the relation lists exactly two jobs, both have a location, neither appears in another relation, neither carries a 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

V2 resources[].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 no objective 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:
For 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:
See Objective function for every cost term.

From weights to prices

V3 has no weights. Every objective term is a price in the objective’s currency: a fleet rate in objective.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

Both examples describe the same route: a 30-minute wait for the first window, a 30-minute break after the first job, and a V3 request that set no objective.

Routes and stops

arrival means different things. V2 visits[].arrival is the service start: the arrival plus any waitTime. V3 stops[].arrival is the physical arrival, before waiting, so the service start is arrival + wait_s. V2 visits have no departure field; V3 adds departure, the end of service.
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 one suggestions[] 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 (always SOLVED), score, messages, unserved, unservedReasons, the four totals, and per trip resource, date, departureTime, travelTime, distance, serviceTime, waitTime, workTime, start, end and visits[]. Every visit carries job, arrival, serviceTime, travelTime, distance, waitTime, breakTime, activity, location, snappedLocation and latlon.
  • score: hardScore and mediumScore are 0 and feasible is true on a solve, because a job that cannot be served is returned in unserved instead of producing an infeasible plan. softScore is 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, hardScore is the negated number of violations, feasible is false when there are any, and softScore is 0.
  • unservedReasons: the values are reason codes such as TIME_WINDOW_VIOLATED or CAPACITY_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 to suggestedArrival), score and an empty violations. scoreExplanation is 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, except deductPreviousBreaks, 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.