Skip to main content
This page is the wire-level companion to Constraint System: which JSON field turns on which constraint, and whether it’s hard (feasibility) or soft (priced into the objective). For the underlying model — constraint evaluation order and how to read the unassigned[] diagnosis — see that page.
The request schema rejects unknown fields. A field this page marks Phase 1 (400) is accepted by the schema but rejected at request time with an error naming the exact field — never a silently ignored option. A field this page doesn’t mention at all is an unknown-field 422.

Overview

Capacity

Every vehicle declares a multi-dimensional capacity; every job/shipment declares a demand. Load never exceeds capacity on any named dimension, at every point along the route.
A demand naming a dimension no vehicle declares is a 400 — you never get a silently-infeasible plan. The response reports load_after (per stop) and load_peak (per route) as the same named map ({ "weight": 30, "volume": 2 }).

Time windows

A job or shipment leg carries one or more time_windows:
Hard by default — arriving outside every listed window makes the task unassignable. Multiple windows mean “any of these is acceptable.” Three separate ways to soften lateness exist, and they compose differently:
number
Grades which of a task’s own alternative windows is preferred — every window stays hard on both sides. Needs at least two windows on the task, and cannot be combined with objective.costs.per_late_hour > 0 anywhere in the request (400) — the two disagree about which window was paid for.
number
Fleet-wide: makes lateness legal-but-priced on every window of every task at once. An explicit 0.0 is rejected as ambiguous — omit the field for hard windows.
object
A per-task soft point deadline, independent of the two mechanisms above: { "at": "2026-04-01T10:00:00Z", "per_late_hour": 60.0 }. Arriving after at is priced, never infeasible; arriving early never forces a wait. Jobs only — 400 if set on a shipments[] entry.
There is no earliness_cost_per_hour / lateness_cost_per_hour pair on time_windows[] — the mechanism above (a fleet-wide rate plus per-window cost and the per-task target_arrival) is what actually ships today.

Shift hours and overtime

A vehicle works only within its shift’s [from, to]; the whole route — including the return leg — must finish by to unless max_overtime_s grants a costed extension.
Overtime is priced via objective.costs.per_overtime_hour (fleet-wide) — components.overtime appears in the response only when that rate is set. Multiple shifts[] on one vehicle model multi-day availability.

Skills

A job’s skills list is an ALL-subset test: the assigned vehicle must provide every named skill. { "name": ... } is the only field on a skill requirement — there is no soft-skill violation_cost, so every skill requirement is hard.

Tags, serves_tags, and preferred_tags

Tags are a separate, ANY-overlap mechanism from skills — for zones and territories rather than capabilities:
shift.serves_tags is hard: when set (non-empty), that shift serves only tasks carrying at least one of the listed tags; untagged tasks remain serveable everywhere. vehicle.preferred_tags is soft: serving a task lacking a preferred tag costs that entry’s violation_cost; entries apply independently, so lacking several preferred tags charges each.

Vehicle eligibility

Three independent mechanisms, from hardest to softest:
  • locked_vehicle — hard-pins the task to one vehicle id; survives re-optimisation. Rejected with 400 if the id matches no vehicle.
  • eligible_vehicles.{allowed,excluded} — hard whitelist/blacklist, mutually exclusive.
  • preferences — soft. Each { vehicle, violation_cost } entry is priced independently: a task listing several preferred vehicles pays every entry whose vehicle didn’t serve it, even if one of the others did.

Route limits

limits.max_distance_m and limits.max_route_duration_s are hard, per-vehicle. There is no separate pure-drive-time cap — max_route_duration_s caps the whole-route span: travel, service, and wait all count. Stop-count is a separate cap, on the shift: shifts[].max_tasks.

Relations

relations[] expresses inter-task ordering, grouping, and synchronisation. All four variants are hard — there is no violation_cost on a relation.
  • ordered — members appear in this order on one route (gaps allowed). Select via job_ids, or a cross-group groups list of tag labels where groups[0] precedes groups[1], and so on (a tag naming a shipment there is 400).
  • same_resource — members served by the same vehicle across shifts/days.
  • same_day — members land on the same calendar day.
  • synchronized — listed tasks start within max_wait_s of each other, across vehicles.
same_resource/same_day select members via job_ids or a single group tag (mutually exclusive). The consecutive and same_route relation types were considered and removed from the schema entirely — an unknown relation type is an unknown-field rejection, not a 400.
Synchronisation (type: "synchronized") is the home-health “two carers, one visit” rule — the hardest constraint for the engine, since it couples two routes.

Shipments

A shipments[] entry’s delivery leg is only ever inserted after its pickup leg, on the same route — same-vehicle and ordering are implicit, never a separate relations[] entry. mandatory/drop_fee/eligible_vehicles apply to the pair as a whole. target_arrival is 400 on shipments today.

Driver breaks

Break is a tagged union: floating (bounded by exactly one windows entry or a drive/duty trigger — EU 561/2006’s 4h30 drive rule is { "after": "drive", "threshold_s": 16200 }) and fixed (a mandatory off-duty interval at a fixed from/to). Breaks are injected into the route timeline and appear as break stops in the response. Phase 1 (400): multiple windows entries on one break, a location on a triggered or fixed break (only windowed breaks may pin one), windows+trigger together, neither on a floating break, and trigger.resets_accumulator: false.

Multi-trip reloads

A vehicle may return to a reloads[] depot mid-shift to reset its load and start a fresh trip. max_trips caps trips (reloads_used + 1) across all reload depots in that shift; a single reload depot can be revisited at most 8 times regardless of max_trips. All reloads[] entries in one shift must currently share the same service_duration_s and cost_per_visit — a per-depot difference is 400. initial_load declares stock already on the vehicle at shift start (occupies capacity on the first trip only, flushed at the first reload).

Reachability

There is no request field for this one: when the fetched distance matrix reports a pair as unreachable (no road connection), the solver activates a constraint that forbids any route from using that leg. A task only reachable through such a leg comes back unassigned with UNREACHABLE_LOCATION.

Constraint System

The full constraint model, evaluation order, and how to read unassigned[].

The V3 API Model

How the full request is structured.

Objective Function

How the soft mechanisms above are priced and ranked.

API Reference

The POST /v3/routing/solve schema.