Skip to main content
The V3 Routing API is a ground-up redesign of how you describe a vehicle routing problem. Instead of dozens of overlapping special-case fields, it exposes a few orthogonal, composable primitives. Everything optional defaults to sensible behaviour, so a minimal request is two objects and a location.
The V3 model is rolling out in phases. The request schema and the core optimisation (named capacity, hard time windows, skills, vehicle eligibility, sequencing, pickup & delivery shipments, the priorities objective) are available now. Advanced capabilities — driver breaks, depot reloads, soft time windows, cross-vehicle synchronisation — are reserved in the schema and currently return a clear 400 naming the field. See Availability below.

Design principles

Few orthogonal primitives

One concept, one mechanism. Vehicle eligibility, skills, and sequencing are each a single field — not five overlapping ones.

Everything soft is money

No abstract weight vector. Lateness costs a rate per hour; a non-preferred vehicle costs an amount. One unit you trade against, and an itemised cost breakdown in every response.

Penalties live on the thing

A preference is a property of a job, so its violation cost sits on the job — read one job and you see exactly what it costs to bend each rule.

Pit of success

Every advanced field is optional with a no-op default. You never see eligible_vehicles or objective until you need them.

A request at a glance

The smallest valid request — minimise total time, serve everything:
{
  "jobs": [
    {
      "id": "j1",
      "location": { "coordinate": [4.35, 50.85] }
    }
  ],
  "vehicles": [
    {
      "id": "v1",
      "shifts": [
        {
          "from": "2026-04-01T08:00:00Z",
          "to":   "2026-04-01T17:00:00Z",
          "start": { "coordinate": [4.30, 50.80] }
        }
      ]
    }
  ]
}
A request has up to seven top-level fields: jobs, vehicles, depots, locations, relations, objective, and options. Only jobs and vehicles are required.

Jobs

A job is a unit of work — a delivery, pickup, service call, or inspection. A single-visit job has its visit fields (location, service_duration_s, time_windows) directly on the object. Shipments use pickup + delivery instead (Coming soon).
{
  "id": "job-42",
  "location": { "coordinate": [4.35, 50.85] },
  "service_duration_s": 600,
  "time_windows": [
    { "from": "2026-04-01T09:00:00Z", "to": "2026-04-01T12:00:00Z" }
  ],
  "demand": { "weight": 30, "volume": 2 },

  "mandatory": true,
  "unassigned_cost": null,

  "skills": [{ "name": "fridge" }],

  "eligible_vehicles": {
    "allowed": ["v1", "v2"],
    "excluded": ["v9"]
  }
}
location
Place
Where the job is performed. A Place is one of:
  • { "coordinate": [lon, lat] } — inline GeoJSON coordinate (Available)
  • { "location_id": "..." } — reference into locations[] (Coming soon)
  • { "depot": "..." } — reference into depots[] (Coming soon)
time_windows
array
One or more time windows. Each window is { "from": "...", "to": "..." } (ISO 8601). Hard by default — arriving outside the window makes the job unassignable.Soft windows with earliness_cost_per_hour / lateness_cost_per_hour are Coming soon.
demand
object
Named multi-dimensional load, e.g. { "weight": 30, "volume": 2 }. Vehicle capacity uses the same dimension names — matched by name, so order never matters and errors name the overflowing dimension exactly. Available.
mandatory
boolean
default:"false"
true = the job must be served; the solve is infeasible if it cannot be. false (default) = droppable. Use unassigned_cost to price the drop. Available.
unassigned_cost
number
Cost of leaving this job unassigned. The solver drops the job only when the saving exceeds this price. Ignored when mandatory: true. Available.
skills
array
Capability requirements: [{ "name": "fridge" }]. The serving vehicle must have all named skills. Hard (no violation_cost) is Available. Soft skills with violation_cost are Coming soon.
eligible_vehicles
object
Hard vehicle filter. allowed is a whitelist (omit = all vehicles eligible); excluded is a blacklist. Available.Soft vehicle preferences (preferences: [{ "vehicle": "v2", "violation_cost": 800 }]) are Coming soon.

Vehicles & shifts

A vehicle carries capacity, skills, a cost model, and one or more shifts. The shift is where the working window and depot locations live.
{
  "id": "truck-1",
  "count": 3,
  "skills": ["fridge"],
  "capacity": { "weight": 1000, "volume": 50 },

  "limits": {
    "max_distance_m": 250000,
    "max_drive_time_s": 28800,
    "max_tasks": 120
  },

  "cost": { "fixed": 100, "per_travel_hour": 50 },

  "shifts": [
    {
      "from": "2026-04-01T08:00:00Z",
      "to":   "2026-04-01T17:00:00Z",
      "start": { "coordinate": [4.30, 50.80] },
      "end":   { "coordinate": [4.30, 50.80] },
      "max_overtime_s": 3600
    }
  ]
}
count
number
default:"1"
Clone this vehicle definition N times. Response carries { "type": "truck-1", "instance": 1 }{ "instance": N } so results stay traceable. Available.
capacity
object
Named dimensions, matched against job demand by name. Available.
limits
object
Per-vehicle route caps. max_distance_m, max_drive_time_s, max_duty_time_s are hard bounds. max_tasks caps the number of stops on the route. Available.
cost
object
Per-vehicle cost rates. fixed is charged when the vehicle is used; per_travel_hour is the travel rate. per_km, per_stop, per_service_hour, per_wait_hour, and per_overtime_hour are Coming soon for the multi-term cost model.
shifts[].start / end
Place
Per-shift depot location. start is where the vehicle departs; end is where it must return. Omit end for an open route (no return leg). Depot references by ID are Coming soon — use inline coordinate for now. Available (inline coordinate).
shifts[].max_overtime_s
integer
How many seconds past to the vehicle may run, priced via cost.per_overtime_hour. Available.
shifts[].breaks
array
Scheduled breaks inserted into the route timeline. Coming soon.
shifts[].reloads
array
Mid-route depot visits to reload capacity. Coming soon.

Relations

A single relations array expresses every inter-job constraint. Each relation is a tagged-union object identified by type. violation_cost absent (or omitted) = hard; a positive value = soft (Coming soon for soft relations).
"relations": [
  { "type": "ordered",       "job_ids": ["a", "b", "c"] },
  { "type": "same_resource", "job_ids": ["f", "g"] },
  { "type": "consecutive",   "job_ids": ["x", "y"], "ordered": true }
]
TypeMeaningStatus
orderedJobs appear in this order on one routeAvailable
consecutiveJobs are adjacent (no stops between them)Available
same_resourceJobs served by the same resource across daysAvailable
same_routeJobs on the same route in one shiftAvailable
same_dayJobs served on the same calendar dayComing soon
synchronizedTwo vehicles meet at a location within max_wait_sComing soon
job_ids entries are polymorphic: a string can be a job id or a tag (which expands to all jobs carrying that tag). An ordered relation over tags orders the groups relative to each other while leaving order within each group free.

Objective

The optimisation goal is one coherent, money-denominated model. The whole block is optional — omit it for “minimise total time, serve everything”.
"objective": {
  "priorities": ["serve_jobs", "minimize_vehicles", "minimize_cost"],
  "costs": {
    "per_travel_hour": 50
  }
}
  • priorities — an ordered list of lexicographic tiers. minimize_vehicles makes the solver use as few vehicles as possible before optimising cost; drop it and vehicle count becomes free.
  • costs — fleet-default rates. per_travel_hour is available now. per_travel_km, per_late_hour, per_wait_hour, and per_overtime_hour are Coming soon for the multi-term cost model.
Soft penalties that belong to a specific job live on that object (skills[].violation_cost, preferences[].violation_cost) — not in a global table.

Response

A solve returns a summary, per-stop routes, and unassigned jobs with structured reasons.
{
  "summary": {
    "status": "solved",
    "jobs_assigned": 47,
    "jobs_unassigned": 1,
    "vehicles_used": 4,
    "total_distance_m": 184320,
    "total_duration_s": 28800,
    "estimated_cost": {
      "total": 43200,
      "components": {
        "travel": 43200,
        "waiting": 0,
        "overtime": 0,
        "lateness": 0,
        "vehicles_fixed": 0,
        "unassigned": 0,
        "preferences": 0,
        "imbalance": 0
      }
    },
    "elapsed_ms": 1050,
    "iterations": 2400
  },
  "routes": [
    {
      "vehicle": { "type": "truck-1", "instance": 1 },
      "distance_m": 46080,
      "duration_s": 7200,
      "overtime_s": 0,
      "stops": [
        {
          "type": "start",
          "location": [4.30, 50.80],
          "departure": "2026-04-01T08:00:00Z"
        },
        {
          "type": "job",
          "id": "job-42",
          "location": [4.35, 50.85],
          "arrival": "2026-04-01T09:00:00Z",
          "departure": "2026-04-01T09:10:00Z",
          "wait_s": 0,
          "service_s": 600,
          "travel_time_s": 3600,
          "load_after": { "weight": 30, "volume": 2 }
        },
        {
          "type": "end",
          "location": [4.30, 50.80],
          "arrival": "2026-04-01T09:40:00Z"
        }
      ]
    }
  ],
  "unassigned": [
    {
      "job_id": "job-99",
      "reasons": [
        { "code": "TIME_WINDOW", "message": "No vehicle can reach the job within its time window." }
      ],
      "relaxations": []
    }
  ]
}
Key response fields:
  • summary.statussolved, partial, or infeasible
  • summary.estimated_cost — always present, itemised by component
  • routes[].vehicle{ "type": "...", "instance": N } for multi-count vehicles
  • routes[].stops[].typestart, task, pickup, delivery, break, reload, end
  • routes[].stops[].load_after — named load map after serving this stop
  • unassigned[].job_id — the id of the unassigned job
  • unassigned[].reasons — structured { code, message } objects (no legacy reason string)

Availability

The V3 schema is stable. Capabilities land in phases — anything not yet wired returns a 400 that names the unsupported field, so you never get a silently-wrong plan.
CapabilityStatus
Single-visit jobs (flat location, service_duration_s, time_windows), named capacity/demand, hard time windowsAvailable
Hard skills (name only), eligible_vehicles (allowed/excluded)Available
mandatory: true, unassigned_cost (droppable at a price)Available
Inline shift depots (start/end coordinate), open routes, countAvailable
max_overtime_s, vehicle limits (max_distance_m, max_drive_time_s, max_tasks)Available
Single-term vehicle cost (fixed, per_travel_hour)Available
ordered (job ids & tag groups), consecutive, same_resource, same_route relationsAvailable
objective.priorities, objective.costs.per_travel_hourAvailable
Itemised estimated_cost + structured unassigned[].reasons in responseAvailable
Soft time windows (earliness_cost_per_hour / lateness_cost_per_hour)Coming soon
Soft skills (violation_cost on skills[])Coming soon
Vehicle preferences (soft affinity, priced)Coming soon
setup_duration_s on jobsComing soon
Multi-term vehicle cost (per_km, per_overtime_hour, per_wait_hour, etc.)Coming soon
Shipments — job.pickup + job.delivery (mandatory, coordinate legs, hard skills)Available
Droppable / soft-skill / multi-vehicle shipmentsComing soon
Driver breaks (shift.breaks)Coming soon
Depot reloads (shift.reloads)Coming soon
Named locations / depot references (location_id, depot in Place)Coming soon
same_day / synchronized relationsComing soon
objective.balance (fairness / workload balancing)Coming soon
Traffic-aware matrices (options.runtime.traffic)Coming soon

API Reference

Full request and response schema for POST /v3/routing/solve.
Migrating from the V2 VRP API? See the V2 → V3 migration guide.