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

# Routing API V3 changelog

> What changed in the V3 Routing API, release by release: breaking changes and what to change, new features, and fixes.

Every change to `/v3/routing/solve`, `/v3/routing/evaluate` and `/v3/routing/suggest` that you can see from
a client, newest first. To hear about new releases, subscribe with the RSS button next to the title.

<Warning>
  V3 is in **beta**. During the beta a release can rename or remove fields, as several entries below do.
  After general availability, planned for January 2027, `/v3` only receives additive changes.
</Warning>

## How to read this page

* Each entry is a release of the routing service, labelled with its date and version. Releases shipped
  on the same day share an entry.
* **Breaking** items need a change in your client, and they say what to change. **New** items are
  opt-in. **Fixed** items correct a defect. **Improved** items change the plans the solver finds, but
  not the contract.
* Field names are today's names. If your integration predates a rename, find the old name in
  [Renamed and removed fields](#renamed-and-removed-fields).
* The page leaves out internal changes with no effect on a request or response. Changes to the
  V2 API (`/v2/vrp/...`) are in the [VRP API changelog](/changelog/vrp).

To check a request against the current contract without fetching travel times or running a search, send it with
`?validate_only=true`. The response lists every problem at once (see [Errors](/guides/vrp/v3/errors)).

## Renamed and removed fields

If you built against an early beta, this table maps every renamed or removed name to today's. Old
request names are rejected with a `400` whose error item is `unknown_field` (or `invalid_type` for an old
enum value or shape, such as a bare-string skill), and the pointer shows where.

### Request

| Before | Now | Since |
| - | - | - |
| `target_arrival` | `target` | v0.11.2 |
| Vehicle `skills` as strings | Objects: `{ "name": "gas", "level": 2 }` | v0.11.2 |
| Flat request body, `initial_routes` | `{ "problem", "plan", "options" }`; a warm start is `plan` | v0.11.3 |
| `POST /v3/routing/validate` | `?validate_only=true` on any of the three endpoints | v0.11.3 |
| `POST /v3/routing/solve/stream` | Removed from the public API | v0.11.3 |
| `options.runtime.engine`, `options.runtime.traffic.departure_time` | `problem.travel.engine`, `problem.travel.departure_time` | v0.11.3 |
| `options.runtime.time_limit_s`, `options.runtime.seed` | `options.time_limit_s`, `options.seed` | v0.11.3 |
| `shifts[].depot_service_time_s` | `shifts[].start.service_duration_s` and `shifts[].end.service_duration_s`, or `depots[].service_duration_s` | v0.11.3 |
| `costs.per_travel_hour` | `costs.per_route_hour` (it prices the whole route span, not only driving) | v0.11.3 |
| `shifts[].start` / `end` as a bare place | `{ "location": [lon, lat] }` or `{ "depot": "<id>" }` | v0.11.3 |
| `shifts[].reloads[]` as `{ location, service_duration_s, cost_per_visit }` | Depot ids, with the depots in top-level `depots[]` | v0.11.3 |
| `initial_routes[]` (`job_ids`, `shift_index`, `reload_index`); evaluate and suggest `routes` | `plan.routes[]` with `vehicle`, `shift` and `stops` | v0.11.3 |
| Suggest `max_suggestions` (top level) | `options.max_suggestions` | v0.11.3 |
| Suggest `new_jobs`, `new_shipments` | `options.tasks` | v0.11.3 |
| Suggest `options.tasks` | Removed: suggest places every task of `problem` that `plan` leaves out | v0.12.0 |
| `objective.costs.per_vehicle` | `objective.costs.fixed` | v0.14.0 |
| `vehicles[].cost` | `vehicles[].costs` (same type as `objective.costs`) | v0.14.0 |
| `objective.costs.currency` | `objective.currency` | v0.14.0 |
| `objective.costs.per_late_hour` | `objective.per_late_hour` | v0.14.0 |
| `costs.per_stop` | Removed | v0.14.0 |
| `shifts[].max_tasks` | `limits.max_tasks`, on the vehicle or on `shifts[].limits` | v0.14.0 |
| `mandatory` | Removed: a task without `drop_fee` is required, one with a `drop_fee` is optional | v0.14.0 |
| `objective.minimize_vehicles` | Removed: price each used vehicle with `costs.fixed` | v0.14.0 |
| `jobs[].demand` | `jobs[].pickup` | v0.14.0 |
| Shipment leg `pickup.demand` / `delivery.demand` | Removed: a shipment's `load` is carried from pickup to delivery; for a swap at one address, use one job with `pickup` and `delivery` | v0.14.0 |
| `breaks[].trigger.deduct_previous_breaks` | Removed | v0.14.0 |
| `shipments[].demand` | `shipments[].load` | v0.14.0 |
| `location: { "coordinate": [lon, lat] }` | `location: [lon, lat]`, in requests and responses | v0.14.0 |
| Break `"type": "floating"` with `windows` | `"type": "windowed"` | v0.14.0 |
| Break `"type": "floating"` with `trigger` | `"type": "triggered"`, with `after`, `threshold_s` and `resets_accumulator` on the break | v0.14.0 |
| `vehicles[].profile` as a free, case-insensitive string | `car`, `truck` or `bike`, lowercase | v0.14.0 |
| Relation `job_ids` (and `synchronized.tasks`) | `stops` | v0.14.0 |
| `same_vehicle` / `same_day` `group` | `tag` | v0.14.0 |
| `sequence.groups` | Its own relation, `{ "type": "tag_order", "tags": [...] }` | v0.14.0 |
| Relation types `ordered`, `same_resource` | `sequence`, `same_vehicle` (also as violation `type`) | v0.14.0 |
| `time_lag.from.id` / `to.id`, relation `at` | `from.stop` / `to.stop`, and `on` | v0.14.0 |
| `target.event: "departure"` | `target.on: "end"` | v0.14.0 |

### Response

| Before | Now | Since |
| - | - | - |
| `summary.estimated_cost`, `summary.total_violations` | `summary.score` | v0.11.3 |
| `estimated_cost.components`: `travel`, `vehicles_fixed`, `preferences`, `target_lateness`, `imbalance` | `score.components`: `time` and `distance`, `fixed`, `tag_preference`, `target`; `imbalance` is removed | v0.11.3 |
| `summary.status` | `summary.tasks` (what is served) and `summary.score.violations` | v0.14.0 |
| `summary.jobs_assigned`, `summary.jobs_unassigned` | `summary.tasks.served`, `.unserved`, `.unserved_required` | v0.14.0 |
| `summary.vehicles_used` (counted routes) | `summary.vehicles_used` counts vehicles; `summary.shifts_used` counts routes | v0.14.0 |
| `summary.total_distance_m` | `summary.distance_m` | v0.14.0 |
| `summary.total_duration_s`; route `duration_s`, `shift_duration_s`, `overtime_s` | `time.travel_s`, `time.span_s`, `time.overtime_s` and the rest of `time`, on the summary and on every route | v0.14.0 |
| `summary.elapsed_ms`, `summary.iterations` | `summary.timings.search_ms`, `summary.timings.total_ms`; `iterations` is removed | v0.14.0 |
| Stop `travel_time_s`, `travel_distance_m` | `travel_s`, `distance_m` | v0.14.0 |
| Suggest `extra_cost`, `extra_distance_m`, `extra_duration_s`, `vehicle`, `position`, `after_job_id`, `before_job_id`, `shift_index`, `trip_index`, `arrival`, `latest_arrival`, `assignments[]` on each option | `delta`, `placements[]` and `routes[]` | v0.13.0 |
| Suggest `interchangeable_vehicles` as vehicle ids | `interchangeable_vehicles` as `{ vehicle, shift }` | v0.13.0 |
| Suggest `suggestions[].options` | `suggestions[].insertions` | v0.15.0 |

## Releases

<Update
  label="October 15, 2026"
  description="v0.15.2 and async runs"
  tags={["Breaking", "New", "Improved"]}
  rss={{
title: "October 15, 2026 (v0.15.2): Async runs, TypeScript client, unknown query parameters rejected",
description: "New: async runs solve in the background with a search of up to an hour; submit, poll, fetch the plan or cancel. New: a beta TypeScript client on npm, and relations that name shipment legs. Breaking: an unknown query parameter is now a 400. The search serves more shipments and handles priced lateness better.",
date: "2026-10-15"
}}
>
  **Breaking**

  * An unknown query parameter is now a `400 invalid_request` with an `unknown_field` item, and a
    `validate_only` that is not a boolean is a `400` with an `invalid_type` item. Before, unknown
    parameters were ignored. Check for misspelled parameters, such as `validateOnly`. The error item has no
    `pointer`, because the parameter is not in the body; `message` names it.

  **New**

  * Async runs: `POST /v3/routing/runs` takes the body of `/v3/routing/solve`, answers `202` at once
    with a run `id`, and solves in the background with an `options.time_limit_s` of up to `3600`
    (solve: `300`). Poll `GET /v3/routing/runs/{id}` until `status` is `succeeded`, `failed` or
    `cancelled`, fetch the plan from `GET /v3/routing/runs/{id}/result`, or stop a run with
    `POST /v3/routing/runs/{id}/cancel`. A body is validated before the run is accepted, so an invalid
    request is a `400` at submit, never a `failed` run. `callback` is not supported yet. See
    [Async runs](/guides/vrp/v3/runs).
  * TypeScript client `@solvice/routing` (beta), typed from the OpenAPI document:
    `npm install @solvice/routing@beta`. It types every request and response and raises error responses
    as exceptions. See [@solvice/routing on npm](https://www.npmjs.com/package/@solvice/routing). Python has no V3
    client yet.
  * `sequence`, `same_vehicle`, `same_day` and `same_route` can name a shipment's legs by their stop ids,
    `pickup.id` and `delivery.id` (by default `"{shipment}:pickup"` and `"{shipment}:delivery"`).

  **Improved**

  * Shipments are now offered routes that are full early on but have room after their peak load. The
    search used to skip those routes, so it served fewer shipments.
  * With `objective.per_late_hour` set, a reordering within a route is now kept only when it lowers the
    route's cost. Before, a move that saved a few metres could add priced lateness.
</Update>

<Update
  label="October 13, 2026"
  description="v0.15.1"
  tags={["Improved"]}
  rss={{
title: "October 13, 2026 (v0.15.1): Fewer unassigned tasks in synchronized and time-lag chains",
description: "When an insertion breaks other chains, the solver now unassigns only the conflicting members instead of rejecting the candidate.",
date: "2026-10-13"
}}
>
  **Improved**

  * `synchronized` and `time_lag` chains: when an insertion pushes members of other chains out of their
    bounds, the solver now unassigns only the members that conflict, rather than rejecting the whole
    candidate. On problems with many chains that span shifts, far fewer tasks are left unassigned.
</Update>

<Update
  label="October 12, 2026"
  description="v0.14.0 and v0.15.0"
  tags={["Breaking", "New", "Fixed"]}
  rss={{
title: "October 12, 2026 (v0.14.0 and v0.15.0): Cost model, field names and result shape",
description: "Breaking: one cost type at fleet, vehicle and shift level; many request fields renamed; datetimes need a UTC offset; ids are limited to 128 characters; solve and evaluate return one result shape; suggest returns insertions with a reason code. New: travel-time estimates when Solvice Maps is unreachable, an all-or-none relation and a per-leg travel cap.",
date: "2026-10-12"
}}
>
  Most of the renames in the [table above](#renamed-and-removed-fields) shipped in these two releases. The
  largest changes:

  **Breaking: request shape**

  * One `Costs` type, used at fleet (`objective.costs`), vehicle (`vehicles[].costs`) and shift
    (`shifts[].costs`) level: `fixed`, `per_route_hour`, `per_travel_km`, `per_wait_hour`,
    `per_overtime_hour`. Each field resolves on its own: the shift's value, then the vehicle's, then
    the fleet's, then the default. A vehicle's `per_route_hour` now **replaces** the fleet rate (it used to
    be added on top of it) and is reported under `score.components.time`. A vehicle that sets its own rate
    still pays the fleet's `fixed`. Any combination of fields is allowed.
  * `costs.per_stop` is removed. `per_late_hour` moves from `objective.costs` to `objective`.
  * New shift-level overrides: `shifts[].capacity` (merged per dimension), `shifts[].limits` (per field)
    and `shifts[].costs` (per field).
  * Breaks are either `windowed` or `triggered` (they used to share one `floating` type). A break window
    is `{ "from", "to" }` only.
  * Every datetime in a request must carry a UTC offset (`2026-10-20T08:00:00+02:00` or `...Z`). A
    datetime without one is a `400` with an `invalid_value` item at that field, and all of them are listed
    in one response.
  * Every `id` (jobs, shipments and their legs, vehicles, quotas) must be 1 to 128 characters long.
  * The day of a shift, a `preferred_date` and a `same_day` group are read in the timestamp's own UTC
    offset. Before, they were read as UTC dates, which moved some shifts to a different day.
  * `options.time_limit_s` takes fractions of a second, with a floor of 0.1 s.

  **Breaking: response shape**

  * Solve and evaluate return the same result type. `summary.status` is gone: read `summary.tasks` for
    what is served, and `summary.score.violations` for broken constraints. On solve that count is always 0.
    On evaluate, it is the number of constraints the plan breaks.
  * `summary.tasks` counts a shipment as one task on both the served and the unserved side.
  * Times in the response (`arrival`, `service_start`, `departure`, and suggest's `arrival` and
    `latest_arrival`) are written in the UTC offset of the serving shift's `from`, not always in UTC. The
    instants are unchanged.
  * New stop field `service_start`: when service begins, after any wait and any break taken on arrival.
  * Suggest results are `insertions` (formerly `options`). When `insertions` is empty, the new
    `reason_code` says why: `time_limit`, `candidates_exhausted` or `infeasible`.

  **New**

  * `options.travel_fallback`: if Solvice Maps cannot be reached, a solve can now estimate travel times
    instead of failing. `estimate` is the default; `fail` keeps the `502`/`503` error. A plan costed on
    estimates has `summary.travel_estimated: true` and no `routes[].polyline`. Evaluate and suggest never
    estimate. See [Distance matrices](/guides/vrp/v3/concepts/distance-matrices).
  * `all_or_none` relation: every listed task is served, or none is. Each member must also be in a
    `synchronized` or `time_lag` relation.
  * `vehicles[].limits.max_leg_travel_time_s`: a hard cap on the travel time of any single leg of a
    route, including the first and last legs.

  **Fixed**

  * `objective.grouping.cost_per_reentry` now accepts up to 1,000 currency units.
  * `time_lag` relations on tasks with more than one time window are scheduled correctly.
  * A `synchronized` stop is no longer held past the end of its shift into overtime.
</Update>

<Update
  label="October 8, 2026"
  description="v0.13.0"
  tags={["Breaking", "New", "Fixed"]}
  rss={{
title: "October 8, 2026 (v0.13.0): Suggest in plan terms, assignment costs, compatible vehicles",
description: "Breaking: each suggest insertion carries a money delta, its placements and the changed routes; quota windows count stops by service start. New: per-vehicle assignment costs and the compatible-vehicles relation.",
date: "2026-10-08"
}}
>
  **Breaking**

  * Suggest answers in the vocabulary of a plan. Each insertion has a `delta` (`cost` in money,
    `distance_m`, `duration_s`), `placements[]` with each added stop's vehicle, shift and arrival
    window, and `routes[]`, the changed plan routes. To apply an insertion, replace the routes of your
    `plan` that have the same `vehicle` and `shift`. Insertions rank by lowest `delta.cost`, and `delta` matches the
    difference between two `/evaluate` calls. See [Suggest](/guides/vrp/v3/suggest).
  * A `quotas[]` window counts a stop when its **service starts** inside `[from, to)`. It used to count every stop
    of a shift whose `from` was in the window. A window inside a single shift is now accepted.

  **New**

  * `jobs[].assignment_costs` and `shipments[].assignment_costs`: an absolute price for serving a task on a
    given vehicle. Vehicles that are not listed cost nothing.
  * `compatible_vehicles` relation, with `vehicles[].compatible_with`: the vehicles that serve the
    relation's tasks must be compatible, which means neither vehicle's `compatible_with` list excludes the other. A
    relation takes 2 to 16 tasks, and each must also be in a `synchronized` or `time_lag` relation: use it
    to choose which crews may work together.

  **Fixed**

  * A windowed break now waits for a service that ends exactly when the break window opens.
</Update>

<Update
  label="October 5, 2026"
  description="v0.12.0"
  tags={["Breaking", "New"]}
  rss={{
title: "October 5, 2026 (v0.12.0): Suggest takes the evaluate body; same-route relation",
description: "Breaking: suggest places every task the plan leaves out, with a 1-second default budget. New: the same-route relation.",
date: "2026-10-05"
}}
>
  **Breaking**

  * Suggest takes the same body as evaluate: `problem` and `plan`, plus optional `options`. Suggest places
    every task in `problem` that `plan` leaves out, so `options.tasks` is removed. A plan that already serves
    every task is a `400`.
  * Suggest's `options.time_limit_s` defaults to 1 second, the same as solve. It used to search until done.
  * A `synchronized` or `time_lag` member that `plan` leaves out, and that suggest cannot place, now
    fails the request with a `400`. Before, it was skipped. Commit it in `plan`, or drop it from `problem`.

  **New**

  * `same_route` relation: every assigned member is served on one route (one vehicle shift). A reload
    within the shift does not split the route.
</Update>

<Update
  label="October 2, 2026"
  description="v0.11.7"
  tags={["New"]}
  rss={{
title: "October 2, 2026 (v0.11.7): Quotas",
description: "New: top-level quotas cap how many of a list of tasks a set of vehicles may serve between them.",
date: "2026-10-02"
}}
>
  **New**

  * Top-level `quotas[]`: a hard cap (`max_tasks`) on how many stops of the listed tasks (`job_ids`) the
    listed `vehicles` may serve between them, optionally within a `from`/`to` window. A shipment counts as
    two stops. Evaluate reports a `quota` violation when a plan exceeds it.
</Update>

<Update
  label="October 1, 2026"
  description="v0.11.6"
  tags={["New", "Fixed", "Improved"]}
  rss={{
title: "October 1, 2026 (v0.11.6): Period limits and per-window lateness",
description: "New: caps across a vehicle's shifts per day, week or explicit period, and priced lateness on a single time window. Fixes to shipment priority and request validation.",
date: "2026-10-01"
}}
>
  **New**

  * `vehicles[].limits.periods[]`: caps on a vehicle's total `max_duration_s`, `max_distance_m` or
    `max_tasks` across all its shifts in a period, either recurring (`every: day` or `week`) or explicit
    (`from`/`to`). Evaluate reports a `period_limit` violation.
  * `time_windows[].per_late_hour`: soft lateness priced on a single task, when the task has exactly one window.

  **Fixed**

  * A shipment's `priority` counts once, matching `score.unserved_priority`. Before, a dropped shipment
    weighed twice as much as a dropped visit with the same priority.
  * New `400`s for values that used to be accepted without effect: negative or non-finite vehicle costs,
    and a negative `max_overtime_s`.
  * An invalid `problem.travel.departure_time` is reported even when the request has other errors.

  **Improved**

  * Tasks coupled by `synchronized` and `time_lag` are placed together, as a unit, when one placement at a
    time cannot fit them.
</Update>

<Update
  label="September 29, 2026"
  description="v0.11.5"
  tags={["New", "Fixed"]}
  rss={{
title: "September 29, 2026 (v0.11.5): Evaluate agrees with solve",
description: "Evaluate uses the solver's own feasibility check, late-hour lateness is charged at the stated rate, and vehicle eligibility is no longer limited to 64 vehicle shifts.",
date: "2026-09-29"
}}
>
  **New**

  * `eligible_vehicles` is no longer limited to fleets of 64 vehicle shifts. The limit is now 256
    distinct vehicle sets across all tasks; see [Limits](/guides/vrp/v3/limits).

  **Fixed**

  * `/evaluate` decides feasibility with the same check the solver uses, so solve and evaluate agree on
    whether a plan is feasible.
  * `per_late_hour` is charged at the rate you set. Before, the objective charged 60 times that rate.
  * The search can no longer return a route that exceeds a vehicle's route limits.
</Update>

<Update
  label="September 27, 2026"
  description="v0.11.3 and v0.11.4"
  tags={["Breaking", "New", "Fixed"]}
  rss={{
title: "September 27, 2026 (v0.11.3 and v0.11.4): V3 contract redesign",
description: "Breaking: nested request bodies, depot and shift ids, a validate_only flag in place of the validate endpoint, one typed error body and one score. New: a category on every unassigned task.",
date: "2026-09-27"
}}
>
  These releases redesigned the V3 contract.

  **Breaking**

  * Request bodies are nested: solve takes `{ problem, plan?, options? }`, evaluate takes
    `{ problem, plan }`, and suggest takes `{ problem, plan, options }`, with `options.tasks` naming the tasks to
    place. The `plan` in a solve response can be
    sent back unchanged to evaluate, to suggest, or to a warm-started solve.
  * Top-level `depots[]`. Every shift needs an `id`. A shift's `start` and `end` take either a `location`
    or a `depot`, and `reloads` lists depot ids. Start, end and reload stops name their depot.
  * `POST /v3/routing/validate` is removed. Add `?validate_only=true` to any endpoint instead; a valid
    request returns `{ "valid": true, "warnings": [] }`.
  * `POST /v3/routing/solve/stream` is no longer part of the public API.
  * One error body on every V3 endpoint: `status`, `code`, `message`, `request_id`, and `errors[]`, with
    one item per problem, each with a `code` and a JSON `pointer` into your request. Schema errors are
    `400`, not `422`. Send an `X-Request-Id` header to trace a request; it is echoed back. See
    [Errors](/guides/vrp/v3/errors).
  * One `summary.score` on solve and evaluate: `violations`, `unserved_priority`, `cost`, `currency` and
    `components`. The components add up to `cost`. See [Scoring](/guides/vrp/v3/concepts/scoring-explanation).
  * `per_travel_hour` is now `per_route_hour`.

  **New**

  * `unassigned[].category`: `impossible`, `blocked`, `priced_out` or `search_limit`. An optional task
    whose `drop_fee` is cheaper than serving it is reported as `priced_out`: raise the fee. It is no longer
    reported as a time-limit problem.

  **Fixed**

  * The unload at the end depot now finishes before the shift ends. Before, only the arrival there was
    checked against the shift end.
  * With `per_late_hour` set, the end of the shift (`to` plus `max_overtime_s`) still holds. Only task windows
    become soft.
  * Responses with many unassigned tasks use far less memory.
</Update>

<Update
  label="September 24, 2026"
  description="v0.11.2"
  tags={["Breaking", "New", "Fixed"]}
  rss={{
title: "September 24, 2026 (v0.11.2): Skill levels, target times and time lags",
description: "Breaking: vehicle skills are objects. New: skill levels, per-vehicle service factors, soft target times, the time-lag relation, and tasks in more than one sequence.",
date: "2026-09-24"
}}
>
  **Breaking**

  * Vehicle `skills` are objects, `{ "name", "level"? }`; a bare string is rejected.

  **New**

  * Skill levels: `jobs[].skills[].min_level`, served by a vehicle skill whose `level` is at least that.
  * `jobs[].service_factors`: a per-vehicle work rate for one task. Its service time is divided by
    `factor` (together with the vehicle's or shift's `service_factor`), so `1.25` makes it 20 % shorter.
  * `jobs[].target`: a soft target time (`at`, `on`, `per_late_hour`). Lateness is priced; a target never
    makes a plan infeasible and never forces a wait. Stops report `target_lateness_s`.
  * `time_lag` relation: a minimum and/or maximum gap between two tasks, on any vehicles, measured from
    the start or end of each.
  * A task can belong to more than one `sequence`.
  * Shift-level `skills`, which replace the vehicle's skills for that shift.
  * `synchronized` can align the ends of service instead of the starts (today `on: "end"`), and stops
    report the wait a synchronization adds as `hold_s`.
  * New limits: 8 tasks per `synchronized` relation, and 16 tasks per group linked by `synchronized` and
    `time_lag` relations. See [Limits](/guides/vrp/v3/limits).

  **Fixed**

  * With `per_late_hour` set, evaluate reports lateness as a cost and no longer as a `time_window` or
    `shift_end` violation.
  * A warm-start `plan` is checked for feasibility. A stop outside `locked_count` that does not fit is
    re-inserted by the search, and a locked stop that cannot be driven is a `400`.
  * Suggest prices its insertions on the schedule after `synchronized` and `time_lag` holds, the same
    schedule evaluate reports.
</Update>

<Update
  label="September 22, 2026"
  description="v0.11.1"
  tags={["Fixed"]}
  rss={{
title: "September 22, 2026 (v0.11.1): Cost and travel reporting fixes",
description: "The reported time cost covers the whole route span, travel through tasks without a location is reported correctly, and requests spanning continents return a 400.",
date: "2026-09-22"
}}
>
  **Fixed**

  * The reported time cost covers the whole route span (travel, service, waiting and breaks), which is
    what the solver optimises. Before, it counted driving time only.
  * Travel to and from a task without a location is reported correctly in stops and totals.
  * Locations on more than one continent return a `400` that explains the problem, instead of a `502`.
</Update>

<Update
  label="September 17, 2026"
  description="v0.11.0"
  tags={["Breaking"]}
  rss={{
title: "September 17, 2026 (v0.11.0): A vehicle without skills has no skills",
description: "Breaking: a vehicle that omits skills can no longer serve tasks that require one.",
date: "2026-09-17"
}}
>
  **Breaking**

  * A vehicle that omits `skills` has no skills, the same as `"skills": []`. Before, it could serve every
    skill requirement. Give each vehicle that needs skills an explicit list.
</Update>

<Update
  label="September 16, 2026"
  description="v0.10.5 and v0.10.6"
  tags={["New"]}
  rss={{
title: "September 16, 2026 (v0.10.5 and v0.10.6): Road data choice and task priority",
description: "New: choose TomTom or OpenStreetMap road data, rank required tasks by priority, and a new suggest engine.",
date: "2026-09-16"
}}
>
  **New**

  * `problem.travel.engine` chooses the road data: `tomtom` (default, typical traffic at the departure time)
    or `osm` (free-flow). `tomtom_real_time` is defined but not enabled yet; it returns
    `403 feature_not_enabled`.
  * `jobs[].priority` ranks required tasks when they cannot all be served: higher-priority tasks are
    kept first (`shipments[].priority` too). It never makes a task optional; `drop_fee` does that.
  * Suggest runs on a new search engine and can place shipments. `extra_cost` is now the exact change in
    plan cost. (These option fields were replaced in v0.13.0.)
</Update>

<Update
  label="September 10, 2026"
  description="v0.10.4"
  tags={["Fixed"]}
  rss={{
title: "September 10, 2026 (v0.10.4): Request bodies up to 10 MiB",
description: "Request bodies between 2 MiB and 10 MiB are accepted, as documented.",
date: "2026-09-10"
}}
>
  **Fixed**

  * Request bodies between 2 MiB and 10 MiB are accepted, as [Limits](/guides/vrp/v3/limits) documents. Before,
    they were rejected with a `400`.
</Update>

<Update
  label="September 7–8, 2026"
  description="v0.10.1 – v0.10.3"
  tags={["New", "Improved"]}
  rss={{
title: "September 7–8, 2026 (v0.10.1 – v0.10.3): Route geometry",
description: "New: road geometry for each route. A solve that starts from a plan returns early once the search stops improving.",
date: "2026-09-08"
}}
>
  **New**

  * `options.polylines`: `simplified` or `full` returns each route's road geometry as `routes[].polyline`.

  **Improved**

  * A solve that starts from a `plan` returns before its time limit once the search stops improving.
</Update>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.