> ## Documentation Index
> Fetch the complete documentation index at: https://docs.solvice.io/llms.txt
> Use this file to discover all available pages before exploring further.

# The V3 API Model

> How the V3 Routing API models a routing problem — a small set of orthogonal, money-denominated primitives designed so the simple case stays simple.

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.

<Note>
  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](#availability) below.
</Note>

## Design principles

<CardGroup cols={2}>
  <Card title="Few orthogonal primitives" icon="cube">
    One concept, one mechanism. Vehicle eligibility, skills, and sequencing are
    each a single field — not five overlapping ones.
  </Card>

  <Card title="Everything soft is money" icon="coins">
    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.
  </Card>

  <Card title="Penalties live on the thing" icon="location-dot">
    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.
  </Card>

  <Card title="Pit of success" icon="feather">
    Every advanced field is optional with a no-op default. You never see
    `eligible_vehicles` or `objective` until you need them.
  </Card>
</CardGroup>

## A request at a glance

The smallest valid request — minimise total time, serve everything:

```json theme={null}
{
  "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).

```json theme={null}
{
  "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"]
  }
}
```

<ParamField path="location" type="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**)
</ParamField>

<ParamField path="time_windows" type="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**.
</ParamField>

<ParamField path="demand" type="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.**
</ParamField>

<ParamField path="mandatory" type="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.**
</ParamField>

<ParamField path="unassigned_cost" type="number">
  Cost of leaving this job unassigned. The solver drops the job only when the
  saving exceeds this price. Ignored when `mandatory: true`.
  **Available.**
</ParamField>

<ParamField path="skills" type="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**.
</ParamField>

<ParamField path="eligible_vehicles" type="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**.
</ParamField>

## 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.

```json theme={null}
{
  "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
    }
  ]
}
```

<ParamField path="count" type="number" default="1">
  Clone this vehicle definition N times. Response carries `{ "type": "truck-1", "instance": 1 }`
  … `{ "instance": N }` so results stay traceable. **Available.**
</ParamField>

<ParamField path="capacity" type="object">
  Named dimensions, matched against job `demand` by name.
  **Available.**
</ParamField>

<ParamField path="limits" type="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.**
</ParamField>

<ParamField path="cost" type="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.
</ParamField>

<ParamField path="shifts[].start / end" type="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).**
</ParamField>

<ParamField path="shifts[].max_overtime_s" type="integer">
  How many seconds past `to` the vehicle may run, priced via `cost.per_overtime_hour`.
  **Available.**
</ParamField>

<ParamField path="shifts[].breaks" type="array">
  Scheduled breaks inserted into the route timeline. **Coming soon.**
</ParamField>

<ParamField path="shifts[].reloads" type="array">
  Mid-route depot visits to reload capacity. **Coming soon.**
</ParamField>

## 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).

```json theme={null}
"relations": [
  { "type": "ordered",       "job_ids": ["a", "b", "c"] },
  { "type": "same_resource", "job_ids": ["f", "g"] },
  { "type": "consecutive",   "job_ids": ["x", "y"], "ordered": true }
]
```

| Type            | Meaning                                             | Status        |
| --------------- | --------------------------------------------------- | ------------- |
| `ordered`       | Jobs appear in this order on one route              | **Available** |
| `consecutive`   | Jobs are adjacent (no stops between them)           | **Available** |
| `same_resource` | Jobs served by the same resource across days        | **Available** |
| `same_route`    | Jobs on the same route in one shift                 | **Available** |
| `same_day`      | Jobs served on the same calendar day                | Coming soon   |
| `synchronized`  | Two vehicles meet at a location within `max_wait_s` | Coming soon   |

<Tip>
  `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.
</Tip>

## Objective

The optimisation goal is one coherent, money-denominated model. The whole block
is optional — omit it for "minimise total time, serve everything".

```json theme={null}
"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.

```json theme={null}
{
  "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.status`** — `solved`, `partial`, or `infeasible`
* **`summary.estimated_cost`** — always present, itemised by component
* **`routes[].vehicle`** — `{ "type": "...", "instance": N }` for multi-count vehicles
* **`routes[].stops[].type`** — `start`, `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.

| Capability                                                                                                              | Status        |
| ----------------------------------------------------------------------------------------------------------------------- | ------------- |
| Single-visit jobs (flat `location`, `service_duration_s`, `time_windows`), named `capacity`/`demand`, hard time windows | **Available** |
| 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, `count`                                                    | **Available** |
| `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` relations                                | **Available** |
| `objective.priorities`, `objective.costs.per_travel_hour`                                                               | **Available** |
| Itemised `estimated_cost` + structured `unassigned[].reasons` in response                                               | **Available** |
| 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 jobs                                                                                              | Coming 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 shipments                                                                        | Coming 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` relations                                                                                   | Coming soon   |
| `objective.balance` (fairness / workload balancing)                                                                     | Coming soon   |
| Traffic-aware matrices (`options.runtime.traffic`)                                                                      | Coming soon   |

<Card title="API Reference" icon="code" href="/api-reference/vrp/v3/solve">
  Full request and response schema for `POST /v3/routing/solve`.
</Card>

Migrating from the V2 VRP API? See the
[V2 → V3 migration guide](/guides/vrp/v3/migration-from-v2).
