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. The tables below show, for each V2 field, the V3 field that replaces it and how the meaning differs. Some V2 fields have no direct V3 equivalent: V2 fields with no direct V3 equivalent lists them and what to send in V3 instead.
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 has no direct equivalent

Check every field your V2 request sends against V2 fields with no direct V3 equivalent. A field in that list has no one-to-one V3 field, 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 to POST /v3/routing/validate/solve. 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 60), 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.
A native plan differs from the V2 job fields in these ways:
  • Shift per route. plan.routes[].shift names the shift each route runs on, so a plan can span several days. V2 has no per-job shift.
  • Order only. A plan lists stops in visiting order, and V3 recomputes every arrival time from that order. V2 initialArrival values are not sent.
  • Freezing. Set locked_count on a plan route to freeze its first stops.
  • Reloads. V2 initialDepot becomes a { "type": "reload", "depot": "<depot id>" } stop in the plan route.
  • Shipments. A shipment appears in the plan as pickup and delivery stops. See Pickup and delivery pairs.
To keep a re-solve close to the previous plan, send that plan back as plan.

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. 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.
  • cost becomes per_reload, charged once per reload. No automatic equivalent: set per_reload yourself.
  • 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. A job with several windows cannot make them soft: per_late_hour is allowed only on a task with exactly one window. To keep a V2 soft window beside hard windows, keep the hard windows and add a target whose at is the soft window’s to. Only lateness past that end is priced. Raise a window’s from to the soft start only if service before it must be forbidden; that changes which plans are feasible. A job can list several windows and is served in one of them. An optional time_windows[].choice_cost ranks them when one window is preferred over another; it cannot be combined with objective.per_late_hour.

Tags

A V3 job has no per-tag hard flag or weight. A tag is a requirement only when you list it in required_skills[], and a preference only when you list it in preferences[]. A V2 tag that you do not port is not enforced: a vehicle without that tag can serve the job. Port every V2 tag explicitly.
When you port natively, decide per tag:
  • A requirement becomes jobs[].required_skills: [{ "name": "plumbing" }]. The vehicle, or the shift that serves the job, must list every required skill. required_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. To keep V2’s ranking, send that product (duration in seconds) as the V3 priority, or send your own rank if job length should not count. priority and drop_cost 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": "departure" measures the end of service instead of the arrival.
A target needs a price you choose. Port the V2 inputs like this:

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 eligible_vehicles: { "allowed": [resource] } on each job instead. Relation fields that change meaning:
  • SAME_TIME tolerance. The V2 reference documents maxWaitingTime (default 1200 seconds). V3 max_spread_s is required: set it explicitly.
  • SAME_TIME with minTimeInterval on three or more jobs: write one time_lag per pair.
  • DIRECT_SEQUENCE time intervals. V2 applies minTimeInterval and maxTimeInterval to DIRECT_SEQUENCE as well as to SEQUENCE. In V3, 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": "departure" 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.
  • Overlapping relations. V2 lets a job sit in several relations of one type. In V3 a job may be named by at most one same_vehicle, one same_day, one same_route and one synchronized relation; a second one is a 400. Merge overlapping same_vehicle or same_route members into one relation. For a job in several SAME_TIME relations, write one time_lag per pair of members instead, with min_s set to minus the tolerance and max_s to the tolerance.
  • 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. To keep that in V3, add an all_or_none relation over the same stops next to the synchronized one. A SAME_TRIP is all-or-nothing in V2 unless it sets partialPlanning: true. V3 has an all-or-nothing form for one shape of it: a pickup-and-delivery pair and a DIRECT_SEQUENCE through the same jobs, with a job such as a tow between the pickup and the delivery. To keep it, add an all_or_none over the shipment id and the other jobs of the consecutive chain, and name the whole chain. Otherwise all_or_none takes jobs that a synchronized or time_lag relation also names, or jobs that form one consecutive chain, 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_cost, required_skills, eligible_vehicles, preferences, service_factors, assignment_costs and target. They apply to the pair.
  • The shipment id must be unique across jobs and shipments.
V2 sets these fields on each of the two jobs and a shipment has them once, so where the two V2 jobs differ, choose the value the pair gets. A V2 relation’s resource becomes eligible_vehicles.allowed with that one id, as does a plannedResource on either job. To keep a pair as two jobs instead, use a sequence and a same_vehicle relation over them.

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_route_duration_s, max_distance_m and max_stops. A shipment counts as two stops toward max_stops. See Constraints.

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 optimized with the V3 defaults, which price distance as well as time. Amounts are in the objective’s currency. A request ported field for field therefore starts paying for distance, which can change which routes win. To optimize route time only, set the distance rate to zero. Rates you leave out keep their defaults:
V2 options.minimizeResources: true becomes a per_route charge, paid once per route, so a vehicle pays it once for every shift it uses. Set objective.costs.per_route to the amount you want to pay to open a route, for example one hour of route time:
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_cost, preferences[].violation_cost, assignment_costs[].cost, target.per_late_hour, time_windows[].per_late_hour). There is no conversion factor from a V2 weight to a price, 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 (objective.costs.per_route_hour, 35 by default). Choose each price against the rate you use. In V3 an unlisted vehicle in assignment_costs costs nothing, while V2 counts an unranked resource as ranking 100: give every vehicle an assignment_costs entry, with the most preferred vehicle at the smallest cost.

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, and the service start is stops[].service_start (arrival + wait_s unless a break is taken on arrival). 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 reports a reload as a visit with activity: "DEPOT" and has no visit for a break.

Summary, score and unassigned jobs

New in V3: summary.tasks.served, tasks.unassigned, 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_cost was cheaper than serving it) or time_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 message) when insertions is empty, placements[].service_start (when service starts, after any wait), insertions[].interchangeable_vehicles, and the top-level truncated and timings.total_ms. See Suggest for every field.

V2 fields with no direct V3 equivalent

The fields below have no one-to-one V3 field. The right-hand column is what to send in V3 instead, or says that there is no equivalent.

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.