Skip to main content
V3 is in beta. General availability (GA) is planned for January 2027.
The V3 Routing API describes a vehicle routing problem with a small set of orthogonal primitives. Every request nests the problem under a problem key; a minimal one is problem.jobs and problem.vehicles.
This page describes the request and response of POST /v3/routing/solve. /v3/routing/evaluate and /v3/routing/suggest take the same problem object. The schema is strict: a field the schema does not define, such as an old field name or a typo, is rejected with HTTP 400 and an unknown_field item. Its message names the field and its pointer locates it: the key itself, or the object that contains it. See Errors.
Throughout this page a task is a job or a shipment.

Design principles

  • Few orthogonal primitives. Eligibility, skills, and sequencing are each a single field. A pickup-then-delivery is its own shipments[] entry type, not a flag on a job.
  • Everything soft is money. Lateness costs a rate per hour, dropping a task costs a fee, a non-preferred vehicle costs an amount. That is one unit to trade against, with an itemised cost breakdown in every response.
  • Penalties live on the thing. A task’s drop price (drop_cost) and preferences sit on the task object itself: read one job and you see what it costs to bend each rule for it.
  • Strict schema, named rejections. Unknown fields are 400s with an unknown_field item. Fields that are specified but not available yet are 400s with a not_supported item, rather than being ignored.

A request at a glance

The smallest valid request serves everything and minimises travel cost:
A solve request has up to three top-level fields: problem, plan, and options. plan is an optional warm start (see Warm-starting with plan) and options is covered under Options and travel. problem holds up to eight fields. vehicles and at least one job or shipment are required; the rest are optional. Conventions:
  • Durations are seconds (_s) and distances are metres (_m).
  • Timestamps are ISO 8601 / RFC 3339 instants. Every timestamp needs a UTC offset (+02:00 or Z): one without is a 400 that names the field. Timestamps in a response are in the UTC offset of the shift they belong to: a route’s stop times and a suggestion’s placement times use the offset of that shift’s from (Z for a UTC from).
  • Coordinates are [longitude, latitude]. A longitude outside -180 to 180 or a latitude outside -90 to 90 is a 400 with out_of_range; [0, 0] is a 400 with invalid_value, because it is the usual placeholder for a missing GPS fix.
  • Ids (jobs[].id, shipments[].id, vehicles[].id, shifts[].id, depots[].id, quotas[].id) are 1 to 128 characters.
  • Money is a plain number in your own currency. Every price and rate must be 0 or more; the ceilings are listed under Limits.

Jobs

A job is a unit of work at one location, such as a delivery, pickup, service call, or inspection. Its visit fields (location, service_duration_s, time_windows) sit directly on the object.
string
required
Unique across jobs and shipments. Echoed as routes[].stops[].id and unassigned[].task.
Place
Where the job is performed: a [lon, lat] array (GeoJSON order). Omit location for a virtual (location-less) job: it still has service duration, time windows, loads, and skills, but requires no travel, and it can share a route with located jobs. A shipment leg always needs a location. A job’s location is always an inline coordinate; only a shift’s start/end can reference a depot by id. See Vehicles and shifts.
integer
Seconds paid once on arrival at a new location, distinct from service_duration_s and skipped when the previous stop in the route is already at this location. Not scaled by service_factor. Default 0; a negative value is a 400.
integer
Nominal on-site service time in seconds. Default 0; a negative value is a 400. The serving vehicle’s service_factor scales it, so the response’s stops[].service_s may differ from the value you sent.
array
One or more { "from", "to" } windows (ISO 8601). Omitted: the job can be served at any time in a shift. Hard by default: a job that cannot be reached inside any of its windows is left unassigned. A window whose from is after its to is a 400 with conflicting_fields. Two optional per-window fields:
  • per_late_hour makes this window soft on its late side: arriving after to is allowed and priced at this rate per hour, in place of the fleet-wide objective.per_late_hour. Must be > 0, and only on a task with exactly one window.
  • choice_cost is a flat price for being served in this window rather than one of the task’s other windows. It grades preference among alternatives (the task needs at least two windows) and does not soften any of them.
See Constraints.
object
Load that goes onto the vehicle at this visit, as named non-negative amounts such as { "weight": 30, "volume": 2 }. It stays on board until the next reload or the end of the route, so load_after rises at the stop. Use it for goods you collect. For goods you drop off, use delivery_load.Dimensions are matched against vehicles[].capacity by name; order never matters. A vehicle whose capacity map omits a dimension has capacity 0 in it; a vehicle with no capacity at all is not capacity-limited. A dimension name that no vehicle lists is therefore not rejected: check for typos if a job comes back unassigned on capacity.
object
Jobs only. Load that comes off the vehicle at this visit, having been loaded at the depot, as named non-negative amounts such as { "weight": 10 }. The vehicle leaves each trip carrying the sum of that trip’s delivery_load quantities, so load_after starts at that sum and falls at each stop. A job can set both fields: delivery_load for what it drops off and pickup_load for what it collects.
The two fields point in opposite directions, and both count against the vehicle’s capacity: On a route that only drops off, or only collects, the peak load is the same total with either field, and only the direction of load_after differs. The choice matters when one route mixes the two: a drop-off frees room for a later collection only when it is modelled as delivery_load. See Capacity.
number
What leaving this task unserved costs, in the objective’s currency, and the only way to make a task droppable: the task is served whenever serving it is cheaper than the fee. From 0 to 1,000,000,000. Fees paid are reported in score.components.drop_costs. Cannot be combined with priority (400).
integer
default:"1"
Importance of a must-serve task when not all of them fit. The solver maximises the summed priority of served tasks before it considers any cost, so a higher-priority task is kept ahead of lower-priority ones. Must be >= 1. It is a rank, not money, and never makes a task droppable. Use drop_cost for that; setting both is a 400. The summed priority of must-serve tasks left out is reported as score.unassigned_priority.
array
Hard capability requirements: [{ "name": "fridge" }], each with an optional min_level (>= 1, default 1). The serving vehicle must provide every named skill at that level or higher. For a soft affinity use tags with the vehicle’s preferred_tags, or preferences (below).
object
Hard filter: allowed (whitelist, omit = all vehicles eligible) or excluded (blacklist), mutually exclusive. An empty allowed list, or a vehicle id that matches no vehicle, is a 400. To pin the job to one vehicle, list only that vehicle in allowed.
array
Soft vehicle affinity: [{ "vehicle": "v2", "violation_cost": 800 }]. Each entry prices independently: a job listing several preferred vehicles pays every entry whose vehicle didn’t serve it. Each violation_cost is 0 or more; a vehicle listed twice, or an unknown vehicle id, is a 400. Reported in score.components.preferences.
array
Absolute price of serving the task on a vehicle: [{ "vehicle": "v2", "cost": 12.5 }]. The vehicle that serves the task pays its cost once (any of its shifts); unlisted vehicles cost 0. Unlike preferences, which charges the vehicles that did not get the task, this prices the one that did, so a task rated badly everywhere is dropped first when capacity runs out. Below priority, and additive with preferences, drop_cost and travel. Each cost is 0 or more; a vehicle listed twice, or an unknown vehicle id, is a 400. Reported in score.components.assignment.
array of strings
Free-form labels used for relation tag matching and, when a shift scopes itself via serves_tags (hard) or a vehicle prices preferred_tags (soft), for tag matching. A task matches when it carries any one of the listed tags, unlike skills, where all must be provided.
object
Soft point deadline: { "at": "...", "on": "arrival", "per_late_hour": 60.0 }. on is arrival (default) or departure (the end of the visit), named after the stop fields; any other value is a 400. at and per_late_hour are required, and per_late_hour must be > 0. Reaching that instant after at is priced, never infeasible, and an early arrival never waits for it. Independent of objective.per_late_hour. On a shipment, measured on the delivery leg.
array
Per-vehicle work-rate factors for this task: [{ "vehicle": "v1", "factor": 1.25 }]. Service time is divided by the factor (above 1 = faster), and it multiplies the serving vehicle’s own rate (the shift’s service_factor if set, else the vehicle’s): service time = service_duration_s / (shift or vehicle factor × factor). Each factor is in [0.001, 5]; an unknown vehicle id, or a vehicle listed twice, is a 400. A vehicle not listed works at its own rate. V2’s durationModifier multiplied the duration instead: when migrating, send 1 / durationModifier here.

Shipments

A shipment is a bound pickup→delivery pair with its own shipments[] entry type. The solver guarantees the pickup precedes the delivery on the same route; carried load counts against capacity only between the two legs.
pickup and delivery each have their own required location, plus setup_duration_s, service_duration_s, time_windows, and an optional stop id (default "{shipment}:pickup" / "{shipment}:delivery"). A stop id, set or defaulted, must not collide with any job, shipment or other stop id (400). The shipment’s load (named non-negative amounts, like a job’s pickup_load) replaces the job load fields. Every other job field (drop_cost, priority, required_skills, eligible_vehicles, preferences, assignment_costs, tags, target, service_factors) is shared and applies to the pair as a whole: one droppability and eligibility decision, one drop_cost charged once. On a shipment, delivery is the delivery stop object shown above. It is not the jobs[].delivery_load load field, which exists on jobs only.
A shipment carries one load from its pickup to its delivery. A swap that collects one quantity and drops off another at the same address is one job with both pickup_load (picked up) and delivery_load (dropped off); see Constraints.
Shipments take part in relations in a limited way. See Relations and shipments.

Vehicles and shifts

A vehicle carries capacity, skills, a cost model, and one or more shifts. The shift is where the working window and start/end locations live. The fleet is one vehicles[] entry per physical vehicle.
string
required
Unique across vehicles[]. Echoed as routes[].vehicle.
string
default:"car"
Routing profile for this vehicle’s travel times and distances: "car", "truck", or "bike". Any other value is a 400.
object
Named dimensions, matched by name against job pickup_load and delivery_load and shipment load, as non-negative integers. Omitted: no load limit in any dimension. A request may use at most four distinct dimension names. See Capacity.
array
Capabilities the vehicle provides: [{ "name": "fridge", "level": 2 }] (level optional, default 1). Always objects: a bare string is rejected. A shift can replace the list for its own day with shifts[].skills.
array
Soft: [{ "tag": "north", "violation_cost": 25.0 }]. Serving a task that lacks a preferred tag incurs that entry’s cost; entries apply independently.
object
Hard caps on every shift of the vehicle: max_stops stops per route (a job counts as one stop, a shipment as two; start, end, break and reload stops do not count), max_distance_m and max_route_duration_s per route (whole-route span: travel, service, and waiting all count, the same quantity as routes[].time.span_s), max_leg_travel_s for any single travel leg (vehicle only, at least 1), and periods[] for caps across all of the vehicle’s shifts in a day, a week, or an explicit window. See Constraints.
object
This vehicle’s own rates: the same fields as objective.costs (per_route, per_used_vehicle, per_route_hour, per_travel_km, per_wait_hour, per_overtime_hour). Each field set here replaces the fleet value for this vehicle; a field not set keeps the fleet value. Nothing is added on top, and each field is reported under the same score.components key as the fleet rate: a vehicle with "per_route_hour": 30 costs 30 per hour of route span, booked in components.time, and still pays the fleet per_route. Any fields combine on one vehicle.
number
default:"1.0"
Travel-speed multiplier, [0.001, 5]. Effective travel time = travel_time / speed_factor, so above 1.0 is faster.
number
default:"1.0"
Work-rate factor for service_duration_s only, [0.001, 5]. Service time is divided by it, so above 1 is faster: effective service time = service_duration_s / service_factor. Never applied to setup_duration_s, depot or reload handling time, waiting, or breaks. A shift’s own shifts[].service_factor replaces it for that day, and a task’s service_factors[].factor for this vehicle multiplies whichever applies.
array of strings
Vehicle ids this vehicle may share the tasks of a compatible_vehicles relation with. Omitted: any vehicle. A list is a veto: two vehicles may share a relation only if neither’s list excludes the other. A vehicle always pairs with itself, so list only its own id to pair it with no other. An empty list, or an unknown vehicle id, is a 400. Has no effect without a compatible_vehicles relation.
string
required
1 to 128 characters, unique within the vehicle. Echoed as routes[].shift.
string (ISO 8601 instant)
required
Start of the working window.
string (ISO 8601 instant)
required
End of the working window. The route, including any unload at end, must finish by to, or by to + max_overtime_s. A to before from is a 400 with conflicting_fields.
object
Where the shift starts: exactly one of location (an inline place) or depot (a depots[].id); both, or neither, is a 400. An optional service_duration_s (seconds, >= 0) overrides the handling time there; it defaults to the depot’s service_duration_s, or 0 for a location, and is not applied on an ignored leg. A shift whose start and end handling alone do not fit between from and to + max_overtime_s is a 400. Omit start for a route that is open on its start side.
object
Where the shift ends, in the same shape as start. Omit end for a route that is open on its end side.
boolean
default:"false"
When true, the leg from start to the first task costs nothing and takes no time, while the start stop is still reported. Use it when the vehicle’s commute should not count. Requires start: true without one is a 400 conflicting_fields.
boolean
default:"false"
The same for the leg from the last task to end; requires end.
integer
Seconds past to the vehicle may run, >= 0. Omitted: to is a hard end. Overtime is priced at the shift’s costs.per_overtime_hour, else the vehicle’s, else objective.costs.per_overtime_hour; while none is set, overtime is allowed at no cost. A synchronized or time_lag hold never runs into overtime: it stops at to.
object
This shift’s capacity, per named dimension. A dimension it names replaces the vehicle’s capacity there; the others keep the vehicle’s value (unlimited when the vehicle sets no capacity).
object
This shift’s max_stops, max_distance_m and max_route_duration_s. Each one set replaces the vehicle’s limits value for this shift; the others keep the vehicle’s. periods[] stays on the vehicle.
object
This shift’s own rates, the same fields minus per_used_vehicle. Each field set here replaces the vehicle’s value (or, when the vehicle does not set it, the fleet’s) for this shift only.
array of strings
Hard scope: when set, this shift serves only tasks carrying at least one of these tags (untagged tasks remain serveable by every shift). Omitted or empty: the shift serves everything.
array
This shift’s skills, in the same { "name", "level" } shape as vehicles[].skills. Replaces the vehicle’s list for this shift only; [] means the shift provides none. Omitted: the vehicle’s skills.
number
This shift’s work rate, replacing vehicles[].service_factor for the shift, in the same [0.001, 5] range and direction (service time is divided by it). Omitted: the vehicle’s value. A task’s service_factors[].factor multiplies it.
array
Scheduled driver breaks (windowed, triggered by drive or duty time, or fixed). They appear in the response as break stops. See Constraints.
array of strings
Depot ids (from depots[]) the vehicle may return to mid-shift to empty its load and start a fresh trip. Each visit takes that depot’s service_duration_s and is charged its per_reload, and appears in the response as a reload stop. Cap the number of trips with max_trips. An unknown depot id, or one listed twice, is a 400. See Constraints.
integer
The most trips the shift may run, across all of its reload depots: trips = reloads used + 1, so 1 allows no reload. At least 1; 0 is a 400. Omitted: trips are not capped, though one reload depot is still visited at most 8 times per shift.
object
Load already on the vehicle at shift start, per named dimension, as non-negative integers. It occupies capacity on the first trip only and is cleared at the first reload. It is not checked against capacity: a value at or above capacity is accepted and shows up as tasks left unassigned, not as a 400.

Depots

problem.depots[] lists the places a shift can start from, end at, or reload at, referenced by id from shifts[].start, shifts[].end, and shifts[].reloads:
Each depot has an id (1 to 128 characters, unique across depots[]), a location, an optional service_duration_s and an optional per_reload:
  • service_duration_s is the handling time per visit in seconds: the load before a shift that starts there, the unload after a shift that ends there, and each reload. Default 0; a negative value is a 400. A shift’s start.service_duration_s or end.service_duration_s overrides it for that end. service_factor never scales it.
  • per_reload is charged once per reload visit, not for starting or ending a shift there, and reported in score.components.reloads. Default 0, at most 1,000,000,000.

Relations

A single relations array expresses every ordering, grouping, and timing rule between tasks. Each relation is a tagged object with a type, and every relation is hard.
same_vehicle, same_day, same_route, time_lag and compatible_vehicles constrain the members that end up assigned: an unassigned member imposes nothing on the others. On evaluate, a plan that breaks a relation reports a violation whose code is the relation’s type; the first-stop pin of a single-entry sequence relation, and the order of a consecutive one, report as sequence. A stop between consecutive members is not reported yet.
These ten are the complete set. Any other type value is a 400 with an invalid_type item. See Constraints for the selection rules and the combinations that are rejected.

Relations and shipments

Relations act on jobs and on shipment legs. A leg is named by its stop id (pickup.id / delivery.id, by default "{shipment}:pickup" / "{shipment}:delivery") and is then related like a job by sequence, same_vehicle, same_day and same_route. A leg coupled to other tasks by synchronized or time_lag, directly or through a sequence, is a 400. What happens when a relation names the shipment itself depends on the type:
No relation constrains a whole shipment; naming one is rejected instead of being dropped after a 200. To order or group a shipment, name its legs in stops; a tag can only select the whole shipment. A shipment’s own pickup-before-delivery order on one route always holds.

Objective

The whole block is optional. Omit it and the solver serves every task it can, then minimises travel cost at the default rates.
  • costs: the fleet rates. vehicles[].costs takes the same fields and shifts[].costs the same minus per_used_vehicle, and each field resolves on its own: the shift’s value, else the vehicle’s, else the fleet’s, else the default.
    • per_travel_km is charged per kilometre driven. Default 0.20.
    • per_route_hour is charged per hour of each route’s span from start to finish (travel, service, waiting, and breaks; routes[].time.span_s). Default 35.
    • per_wait_hour and per_overtime_hour are extra rates for waiting time and for time past a shift’s to. Default 0.
    • per_route is a flat charge for each route that serves at least one task (a vehicle has one route per shift). Default 0.
    • per_used_vehicle is a flat charge once for each vehicle that serves at least one task on any shift, however many shifts it works. It adds to per_route, exists on objective.costs and vehicles[].costs but not on shifts[].costs, and defaults to 0.
    The defaults apply per field: setting one rate leaves the others at their defaults. An explicit 0 prices that term at zero. Every value must be 0 or more. Rates are rounded to 1/100,000 of a currency unit per metre or per second, and a non-zero rate that rounds to zero is a 400 rather than silently becoming free.
  • currency: an informational label echoed as score.currency. It does not convert anything.
  • per_late_hour: prices lateness on every time window that does not carry its own time_windows[].per_late_hour, making those windows soft on the late side. Omit it to keep windows hard; an explicit 0 is a 400.
  • per_delay_workday: ASAP scheduling. Money charged for each served task per workday its shift lies after the earliest workday in the request. A workday is a distinct date, in the shift’s own UTC offset, on which at least one shift starts; shifts on one date share a rank. Unserved tasks pay nothing; the charge never outranks priority, and it cannot be combined with drop_cost (a 400). Omit it or send 0 to switch it off. When every shift starts on one date it charges nothing, but the drop_cost rejection still applies. Reported in score.components.delay.
  • grouping: keeps tasks that lie within radius_m of each other consecutive in a route by charging per_entry (required, 0 to 1000) on every entry into such a group from a job outside it, the first entry included; entering from the shift’s start or a reload is free. Present = enabled. radius_m defaults to 180 and must be > 0. distance_metric selects how the radius is measured: straight_line (default) or road.
  • workload_share: spreads service time across vehicles. Each vehicle’s scheduled service time (travel, setup, waiting and breaks are not counted) is compared with its share of all the work, and the part outside a band of tolerance (default 0.1) is charged per_imbalance_hour. Present = enabled; vehicles[].workload_weight scales a vehicle’s share (0 leaves it out). Reported as score.components.workload_share.
  • Vehicle count: there is no separate flag for it. The number of vehicles used is whatever minimises total cost. To consolidate work onto fewer vehicles, put a price on using one: costs.per_route for each used shift, or costs.per_used_vehicle once per vehicle however many shifts it works, on the whole fleet (objective.costs) or per vehicle.
Soft penalties that belong to a specific task live on that object (drop_cost, preferences[].violation_cost, target.per_late_hour, time_windows[].per_late_hour), not in this block. See Objective function for how plans are ranked, and Constraints for every hard and soft rule.

Options and travel

options sits next to problem at the top level of a solve request:
number
default:"150 ms plus 4 ms per task"
Search time budget in seconds; fractions are allowed (2.5). Omitted, it is 150 ms plus 4 ms per task (tasks are jobs and shipments together). Maximum 60; a larger or negative value is a 400. A value below 0.1 is raised to a 100 ms minimum. This is search time only: the travel-matrix fetch and any wait for capacity come on top.
integer
default:"42"
Random seed for the search. Repeated runs with the same seed on the same hardware usually return the same plan. Omitting seed is the same as sending 42, so to explore other plans you have to send different values.
string
simplified or full. When set, each route carries its road geometry as routes[].polyline (an encoded polyline, precision 5). Omitted = no geometry.
string
default:"estimate"
What a solve does when travel times cannot be fetched from the travel data provider, whatever the reason (outage, timeout, overload, an error from the provider). estimate solves on travel estimated from straight-line distance and a typical speed for the vehicle profile, and sets summary.travel_estimated to true. fail returns the 502 or 503 instead. Coordinates the provider rejects for spanning more than one region are a 400. Evaluate and suggest have no such option and always estimate. See Distance matrices.
/v3/routing/suggest has its own options: max_insertions (how many ranked insertions to return per task, 5 by default and at most 10) and time_limit_s (the search budget, default 1 second). The tasks to place come from problem, not from options. See Suggest. /v3/routing/evaluate takes no options. See Limits for request size and count caps, and the Performance guide for choosing a time budget. problem.travel controls where travel times and distances come from:
string
tomtom uses typical traffic for the departure time. osm uses free-flow times with no traffic. straight_line uses great-circle distance at travel.speed_kmh, with no road network. Omitted = osm, or tomtom when departure_time is set. tomtom_real_time is not enabled and returns 403 feature_not_enabled.
number
default:"50"
The speed for engine: straight_line, from 1 to 1000 km/h. Rejected with 400 alongside any other engine.
string (ISO 8601 instant)
The instant the traffic lookup is made for, such as tomorrow’s 08:00 to plan with morning-rush travel times. Omitted = free-flow times, unless engine: tomtom asks for traffic at the time of the request. Rejected with 400 alongside engine: osm or engine: straight_line. A request that sets it skips the server-side travel-matrix cache, so it fetches fresh travel data every time.
See Distance matrices for how travel data is looked up and what it drives in the plan.

Response

A solve returns a summary, per-stop routes, the same routes as a plan, and unassigned tasks with structured reasons. The example below serves two jobs on one route with a driver break and a mid-shift reload, and leaves a third job unassigned. It is a standalone illustration, priced at the default rates (35 per hour, 0.20 per km) with no per-vehicle or depot cost:
The sections below list every field of a solve response. Solution quality explains how to judge a plan from them.

Summary

object
{served, unassigned, unassigned_required}. A job is one task and a shipment is one task. unassigned is the length of unassigned[]; unassigned_required counts the unassigned tasks with no drop_cost. A plan that leaves tasks out is still HTTP 200: check tasks rather than the status code.
{termination, iterations}: how the search ended. termination is time_limit (the search used up its budget: options.time_limit_s, or the lower cap a one-vehicle solve with time windows gets), no_improvement (it stopped early because the best solution stopped improving; not a proof of optimality; warm-started and some one-vehicle solves can end this way) or not_searched (/evaluate, with iterations 0). See Performance. New values may be added: accept unknown ones.
integer
The number of distinct vehicles with at least one route.
integer
The number of entries in routes[]. A route is one vehicle on one shift, so a vehicle that works two shifts counts twice here and once in vehicles_used.
integer
The sum of routes[].distance_m, in metres.
object
The sum of routes[].time, in seconds: span_s, travel_s, service_s, depot_s, wait_s, break_s and overtime_s. span_s is the wall-clock span the time cost component is charged on; travel_s is driving time only. wait_s is the sum of the stops’ wait_s, the idle before a windowed break’s window opens included, so it can exceed the waiting cost component and what per_wait_hour charges for. The parts sum to span_s with two exceptions: setup time is inside the span with no field of its own, and a fixed break that begins while the vehicle is driving overlaps that drive, so the overlap counts in both travel_s and break_s.
object
violation_count, unassigned_priority, cost, currency, and components. violation_count is always 0 on a solve. unassigned_priority is the summed priority of must-serve tasks left unassigned (a task that priced a drop_cost counts in components.drop_costs instead). Every component is always present and the components sum to cost. currency appears only when you sent objective.currency. See Objective function.
object
total_ms is the wall-clock time the request took, in milliseconds; search_ms is the part spent in the search (absent on evaluate).
boolean
true when travel times could not be fetched from the travel data provider and the plan was built on estimated travel (straight-line distance with a detour factor, at a typical speed for the profile, with no one-way streets or traffic). The plan, unassigned[].reasons and score.violation_count hold only against that estimate, and no routes[].polyline is returned. Absent when travel came from the road network. On a solve, set options.travel_fallback to fail to get an error instead; evaluate always estimates.
The summary of an evaluate response has the same shape.

Routes

routes[] has one entry per route that serves at least one task. A shift with no work has no entry.
string
The vehicle id from the request.
string
The id of the shift this route runs on.
integer
Total distance driven on the route, in metres.
integer
Driving time only, in seconds: the sum of the stops’ travel_s.
integer
The route’s span in seconds, from the start stop’s arrival to the end stop’s departure. It includes service, waiting, and breaks, and is what per_route_hour is charged on.
integer
Seconds the route runs past the shift’s to; 0 when it finishes within the shift.
object
The highest load reached on the route, as a named map. Absent when the request names no load dimension.
array
The stops in time order (by departure), including the start and end stops. A windowed break taken while the vehicle is at a stop is listed just before that stop and lies within that stop’s arrival to departure, the end stop included. Drive, duty and unavailability breaks end before the next stop’s arrival. See Stops.
string
The route’s road geometry as an encoded polyline (precision 5). Present only when you set options.polyline, and absent for a route whose geometry could not be produced. distance_m and time come from the travel data the plan was built on, so rely on them if they differ from the geometry.

Stops

string
One of start, job, pickup, delivery, break, reload, end. Every route has one start and one end. pickup and delivery are the two legs of a shipment.
string
On a job stop, the job id. On a pickup or delivery stop, that leg’s own id, or "{shipment}:pickup" / "{shipment}:delivery" when it has none. On a break stop, the id of the request break (shifts[].breaks[].id) it realises, absent when that break named none.
string
The depots[].id of the stop: on every reload stop, and on a start or end stop when the shift names a depot there.
array
The stop’s coordinate as [longitude, latitude]. Absent on break stops and on the start or end stop of a route that is open on that side. A virtual job reports the coordinate of its nearest located neighbour on the route: the previous located stop, else the next one. It is absent only on a route with no located stop at all.
array
The same point snapped to the road network, when available.
string (ISO 8601 instant)
Arrival at the stop, before any waiting. Service starts at service_start. On a break stop, when the break starts. Always present on every stop. In the UTC offset of the route’s shift from: a shift starting 2026-07-01T08:00:00+02:00 reports 2026-07-01T08:10:00+02:00. A shift from in UTC reports Z.
string (ISO 8601 instant)
When service begins, on task and reload stops: arrival + wait_s plus the duration of any windowed break taken on arrival there. In the same offset as arrival.
string (ISO 8601 instant)
Departure from the stop, in the same offset as arrival.
integer
Seconds spent waiting before service starts: for the time window to open, for any hold reported in hold_s, and for a windowed break’s window to open when the break is taken on arrival there. On an end stop, the idle time at the end, never negative: departure - arrival - service_s less the service_s of break stops taken there. Idle before a break’s window opens is charged through the span, not by per_wait_hour.
integer
The part of wait_s spent holding for a synchronized or time_lag partner on another vehicle. Present only when it is above 0.
integer
Time spent at the stop, in seconds. On a task stop this is the effective service time, after the vehicle’s service_factor, so it can differ from the service_duration_s you sent; setup_duration_s is not included. On a start, end, or reload stop it is the depot handling time. On a break stop it is the length of the break, so departure - arrival == service_s. service_factor never scales depot, reload or break time.The timeline reconciles per stop. On a task or reload stop, departure - service_start - service_s - break_s is the stop’s setup time (break_s is absent, so counts as 0, on a stop without an interrupting break): the request’s setup_duration_s for the first stop of a run at the same location (setup is charged when the previous stop is elsewhere), and 0 for a co-located successor. service_start is arrival + wait_s plus the duration of any windowed break taken there. On a start or end stop, departure - arrival - wait_s - Σ service_s of the windowed break stops taken there == service_s (a start has no wait).
integer
Seconds of driver break taken in the middle of this stop’s service, pausing it. Only a job with break_interruptible: true can be paused: a fixed, windowed or triggered break that falls inside its service gets no break stop of its own and is counted here and in the route’s time.break_s. Present on job stops only, when above 0.
integer
Driving time from the previous stop, in seconds.
integer
Distance from the previous stop, in metres.
integer
Seconds the start of service, measured from the arrival plus any windowed break taken on it (idle until its window opens included), is past the task’s time window; 0 when on time. A target is reported separately, in target_lateness_s.
integer
Seconds past the task’s target.at, measured at arrival (plus any windowed break taken there, idle before it included) or departure as target.on says; 0 when met. Present only on a stop whose task has a target. For a shipment it is on the delivery stop.
object
The load on the vehicle after this stop, as a named map. Absent when the request names no load dimension.
Every stop is the same flat object. type, arrival and departure are on every stop, and the schema marks only those three as required; the other fields appear on the stop types below and are absent elsewhere: routes[].violations and routes[].stops[].violations appear only in an evaluate response. A solve response never has them.

Plan

object
The same routes in the request’s plan vocabulary: plan.routes[], each with vehicle, shift, and stops[] of type job, pickup, delivery, or reload. Feed it straight into evaluate, suggest, or a warm-started solve. See Warm-starting with plan.

Unassigned tasks

unassigned[] has one entry per job or shipment that is not on a route. It is an empty array when everything is assigned.
string
The id of the unassigned job, or the shipment id for a shipment.
string
Always present, one of: impossible (no vehicle can ever serve it), blocked (a vehicle could serve it alone, but not alongside the tasks the plan serves), priced_out (paying its drop_cost was cheaper than serving it), or time_limit (a worthwhile placement exists but was not found within the time budget, the one case a longer time_limit_s can help).
array
{ constraint, message } objects, one per rule that refused the task, named in constraint from the same vocabulary as evaluate’s violation code (time_window, capacity, required_skills, …). priced_out and time_limit carry one reason without a constraint. New values may be added, so accept unknown ones. message is for people. See Reasons.
array
Best-effort suggestions for a change that would let the task be served. Empty when no simple relaxation exists. Each entry has:
  • op: extend_time_window or reduce_load.
  • amount and unit: the size of the change. unit is s for seconds on extend_time_window and load on reduce_load.
  • pointer: a JSON Pointer to the request field to change: /problem/jobs/{i}/time_windows or /problem/jobs/{i}/pickup_load (/delivery_load when the job only delivers) for a job, /problem/shipments/{i}/pickup/time_windows or /problem/shipments/{i}/load for a shipment.
  • message: the same advice as a sentence for people.

Search progress and warnings

Both are top-level fields, not under summary.
array
How the best plan improved during the search, oldest first, as {elapsed_ms, unassigned_priority, cost} entries. The first is the plan the search started from, each one after it is a better plan, and the last is the final plan: it equals summary.score, and its elapsed_ms equals summary.timings.search_ms. It can equal the entry before it, or be slightly worse. elapsed_ms counts from the start of the search; unassigned_priority and cost mean what they mean in summary.score. At most 500 entries, a longer curve thinned evenly; some large solves without a plan have the final entry only. Present on every solve, absent on evaluate. See Performance for using it to choose time_limit_s.
array
Inputs your request carried that changed nothing, as {code, pointer, message} items. Always present, [] when there are none; /suggest returns it too. At most 100 are returned. No input produces one today; new codes may be added, so accept unknown ones.

Pointers

errors[].pointer (on a 400) and unassigned[].relaxations[].pointer (on a 200) are both JSON Pointers from the root of the body you sent, so they start with /problem, /plan or /options and name a task by its index in the array: /problem/jobs/1/priority, /problem/shipments/0/load. See Errors for the pointer rules.

Warm-starting with plan

The request-level plan seeds the search from an existing plan (re-optimisation) instead of constructing from scratch:
Each route names a vehicle and a shift id (the vehicle’s shift this plan fills), an ordered stops list, and an optional locked_count: how many leading stops the search must not move (a dispatched prefix; cannot be combined with a reload stop on the route, and cannot exceed the route’s number of stops). A task stop has a type of job, pickup, or delivery and an id, as in { "type": "job", "id": "job-1" }. A shipment leg’s id is its own id, or "{shipment}:pickup" / "{shipment}:delivery" when it has none. A reload stop is { "type": "reload", "depot": "..." }, naming a depot from that shift’s reloads and splitting the route into a new trip. Start, end, and breaks are placed by the solver and are never listed. plan is the same shape a solve response returns, so result.plan goes back in unchanged. A plan that names an unknown vehicle, shift, depot, or stop id, lists a stop twice, or is otherwise malformed is rejected with 400. The item’s pointer is /plan/routes/{i} or, for a stop-level problem, /plan/routes/{i}/stops/{k}. plan is a hint, not a guarantee on solve:
  • An unlocked stop that violates a hard constraint (capacity, skills, eligibility, a limit, or a hard time window) is taken out of its position. The search reinserts it wherever it fits, or it appears in unassigned[] with a reason.
  • A locked stop (inside locked_count) that violates a hard constraint can be neither moved nor dropped, so the request fails: 400, item code invalid_value, pointer /plan, with a message naming the stop, the vehicle, and the rule it breaks.
  • Time windows are hard by default, so a stop the plan serves after its window closes is treated as violating. Price lateness (time_windows[].per_late_hour or objective.per_late_hour) and the stop is kept.
The checks on a plan’s routes and stops run after the travel matrix has been fetched, on all three endpoints, so POST /v3/routing/validate/* does not cover them. To score a plan as it stands, without searching for a better one, send it to POST /v3/routing/evaluate. It fetches the travel matrix like a solve does, then returns the plan’s timing, loads, and score, with each broken rule listed as a violations[] entry on the stop or route instead of dropping the stop. See Evaluate.

Errors

The three endpoints report errors in one JSON body. A 401 and a 429 are the exceptions: they are answered before the request reaches the routing service and can have a different body or none, so branch on the HTTP status for those two. See Errors. A request that does not parse or does not validate is HTTP 400:
  • errors[] has one item per problem found. errors[].code says what kind of problem it is (unknown_field, missing_field, invalid_type, invalid_value, not_supported, out_of_range, limit_exceeded, …).
  • errors[].pointer, when present, is a JSON Pointer into the request body you sent, such as /problem/depots/0/service_duration_s, /plan/routes/0/stops/2, or /options/time_limit_s. It usually ends at the offending key and sometimes at the object that contains it; the message always names the field. relaxations[].pointer uses the same notation: see Pointers.
  • With one item, the top-level message repeats that item’s message; with several it reads "{n} problems in request".
  • Branch on code and errors[].code; the messages are for people.
See Errors for every item code, retry guidance, and authentication failures.

Constraints

Every constraint field, hard vs. soft, with examples.

Evaluate

Score an existing plan and list the rules it breaks.

Suggest

Rank insertion positions for new tasks in an existing plan.

Objective function

How plans are ranked and how score is built.

Solution quality

How to judge a plan from the response fields.

Distance matrices

Where travel times and distances come from.

Migration from V2

Field-by-field mapping from the V2 VRP API.

API reference

Full request and response schema for every endpoint.