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

# Validate

> Pre-matrix check of request shape, referential integrity and unsupported features — no matrix fetch, no solve



## OpenAPI

````yaml vrp-v3 POST /v3/routing/validate
openapi: 3.1.0
info:
  title: Solvice Routing Solver
  description: >-
    Optimizes vehicle routes to minimize distance, time, or fleet size while
    respecting capacity, time windows, skills, and custom constraints.


    **Authentication:** send your Solvice API key (or Auth0 JWT) in the
    `Authorization` header. In production, requests are routed through the
    Solvice gateway at `api.solvice.io`.
  contact:
    name: Solvice
    url: https://www.solvice.io
  license:
    name: Proprietary
  version: 3.0.0
servers:
  - url: https://api.solvice.io
    description: Production — Solvice API gateway
security:
  - apiKey: []
tags:
  - name: Routing
    description: Vehicle routing optimization
  - name: Operations
    description: Health and diagnostics
paths:
  /v3/routing/validate:
    post:
      tags:
        - Routing
      summary: Validate a solve request
      description: >-
        Runs pre-matrix request validation (shape, referential integrity,
        unsupported features) without fetching a matrix or solving. 200 = /solve
        would very likely accept it; 400 = the same error body /solve would
        return for this rejection.
      operationId: validate_handler
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SolveRequest'
        required: true
      responses:
        '200':
          description: Request is valid
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '415':
          description: Content-Type not application/json
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '422':
          description: Body could not be deserialized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
components:
  schemas:
    SolveRequest:
      type: object
      description: Top-level request body for `POST /v3/routing/solve` (schema v2).
      required:
        - vehicles
      properties:
        initial_routes:
          type:
            - array
            - 'null'
          items:
            $ref: '#/components/schemas/RouteAssignment'
          description: >-
            Warm-start: route assignments the search starts from instead of

            constructing from scratch (re-optimization). Unknown vehicle/job ids

            are rejected with HTTP 400. See [`RouteAssignment::locked_count`]
            for

            freezing a dispatched prefix. Accepted for solve-payload
            compatibility

            on `/v3/routing/evaluate` and `/v3/routing/suggest` and ignored
            there

            — `routes` is the input on those endpoints.
        jobs:
          type: array
          items:
            $ref: '#/components/schemas/Visit'
          description: Single-stop visits to serve.
        objective:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/Objective'
              description: Objective configuration.
          description: Objective configuration.
        options:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/Options'
              description: Solver runtime options.
          description: Solver runtime options.
        relations:
          type:
            - array
            - 'null'
          items:
            $ref: '#/components/schemas/Relation'
          description: Ordering, grouping, and synchronization constraints.
        shipments:
          type: array
          items:
            $ref: '#/components/schemas/Shipment'
          description: Bound pickup→delivery shipments to serve.
        vehicles:
          type: array
          items:
            $ref: '#/components/schemas/Vehicle'
          description: Available vehicles.
      additionalProperties: false
      example:
        jobs:
          - demand:
              weight: 20
            id: job-1
            location:
              coordinate:
                - 3.725
                - 51.05
            service_duration_s: 300
            skills:
              - name: fridge
            time_windows:
              - from: '2026-07-01T09:00:00+02:00'
                to: '2026-07-01T12:00:00+02:00'
          - demand:
              weight: 35
            id: job-2
            location:
              coordinate:
                - 3.71
                - 51.06
            service_duration_s: 600
        objective:
          costs:
            currency: EUR
            per_travel_hour: 3600
          minimize_vehicles: true
        options:
          runtime:
            seed: 42
            time_limit_s: 5
        vehicles:
          - capacity:
              weight: 500
            id: v1
            profile: car
            shifts:
              - end:
                  coordinate:
                    - 3.7174
                    - 51.0543
                from: '2026-07-01T08:00:00+02:00'
                start:
                  coordinate:
                    - 3.7174
                    - 51.0543
                to: '2026-07-01T17:00:00+02:00'
            skills:
              - fridge
    ErrorResponse:
      type: object
      description: |-
        Top-level error response body.

        Flat envelope matching the Spring Boot / Spring Cloud Gateway default,
        so clients see a single shape across the Solvice platform regardless of
        whether the gateway or the routing-solver produced the error.

        Example:
        ```json
        {
          "message": "At least one vehicle is required",
          "status": "BAD_REQUEST",
          "code": "VALIDATION_ERROR",
          "field": "/vehicles"
        }
        ```
      required:
        - message
        - status
      properties:
        code:
          type:
            - string
            - 'null'
          description: >-
            Stable machine-readable error code. Closed set of exactly two
            values:

            `"UNSUPPORTED_FEATURE"` (a schema field the solver does not honor
            yet —

            the request is valid, the feature is not live) and
            `"VALIDATION_ERROR"`

            (an invalid or inconsistent value). Populated on both the V3-mapping
            and

            the plain request-validation paths (400s); `None` for every other
            error

            variant (matrix/internal/queue-full errors, malformed JSON, etc.),
            so

            branch on the HTTP status first and on `code` only for 400s.
          example: VALIDATION_ERROR
        field:
          type:
            - string
            - 'null'
          description: >-
            JSON pointer to the offending request field, when the message names
            one

            (e.g. `"/vehicles/0/profile"`). `None` when not field-specific.
          example: /vehicles
        message:
          type: string
          description: Human-readable error description.
          example: At least one vehicle is required
        status:
          type: string
          description: Textual HTTP status (e.g. `"BAD_REQUEST"`, `"SERVICE_UNAVAILABLE"`).
          example: BAD_REQUEST
        value:
          description: The offending value, when available.
      example:
        code: VALIDATION_ERROR
        field: /vehicles
        message: At least one vehicle is required
        status: BAD_REQUEST
    RouteAssignment:
      type: object
      description: >-
        A route assignment: an ordered list of job ids served by one vehicle.


        Imports an existing solution into `POST /v3/routing/evaluate` /

        `POST /v3/routing/suggest`. Jobs not listed in any assignment are
        treated as

        unassigned. `vehicle` is the request `vehicle.id` (matching a solve

        response's `Route.vehicle`, which is the same plain id string).
      required:
        - vehicle
        - job_ids
      properties:
        job_ids:
          type: array
          items:
            type: string
          description: >-
            Ordered task ids for this trip. A plain job id appears once; a
            shipment

            id appears exactly twice — first occurrence is the pickup leg,
            second is

            the delivery leg (both legs must land on the same `(vehicle,
            shift_index)`).
          example:
            - job-1
            - job-2
        locked_count:
          type:
            - integer
            - 'null'
          format: int32
          description: >-
            Number of leading `job_ids` locked in place (warm-start prefix the
            solver

            must not move). Only meaningful on `POST /v3/routing/solve`
            `initial_routes`;

            inert on evaluate/suggest. Counts `job_ids` entries, so a locked
            shipment

            costs two. Default 0 (all movable).
          example: 0
          minimum: 0
        reload_index:
          type:
            - integer
            - 'null'
          format: int32
          description: >-
            The reload this trip departs from, as a 0-based index into

            `shifts[shift_index].reloads[]`. Set on — and only on — the
            non-first

            trips of a multi-trip shift: the first trip leaves the shift `start`

            depot and omits this; each later trip names the reload it restarts
            from.

            Absent (default) means a single-trip route. An index rather than a
            depot

            id because inline `Shift.reloads[]` entries carry no id today.
          example: 0
          minimum: 0
        shift_index:
          type:
            - integer
            - 'null'
          format: int32
          description: >-
            0-based index into this vehicle's `shifts[]`, selecting which shift
            this

            assignment fills. Default `0`. Repeated assignments sharing the same

            `(vehicle, shift_index)` are consecutive trips of that shift. Named

            `_index` because shifts are anonymous positional entries (no shift
            id).
          example: 0
          minimum: 0
        vehicle:
          type: string
          description: Vehicle id serving this route (must match a request `vehicle.id`).
          example: v1
      additionalProperties: false
      example:
        job_ids:
          - job-1
          - job-2
        vehicle: v1
    Visit:
      type: object
      description: |-
        A single-stop visit: one location served once (the `jobs[]` entry type).

        A `Visit` has no `pickup`/`delivery` field, so shipment shapes are
        unrepresentable here — illegal job states cannot be expressed.
      required:
        - id
      properties:
        break_interruptible:
          type:
            - boolean
            - 'null'
          description: >-
            **Phase 1:** setting `false` is rejected with HTTP 400 (the default
            `true` is supported).

            Whether a break may interrupt service here. Default `true`.
          example: true
        delivery:
          type:
            - object
            - 'null'
          description: >-
            Quantity delivered at this visit — comes off the truck here, having
            been loaded at a depot — as non-negative magnitudes. Direction is
            carried by the field name, never by sign. Absent or empty means no
            delivery at this visit.
          additionalProperties:
            type: integer
            format: int32
            minimum: 0
        demand:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/BTreeMap'
              description: >-
                Load on the vehicle (for shipments: carried between pickup and
                delivery).


                On a [`Shipment`] this is the **symmetric** form — one quantity
                picked

                up and the same quantity dropped off. For a cargo swap whose two
                legs

                move different amounts, set [`Stop::demand`] on both legs
                instead;

                combining the two forms is rejected with HTTP 400.
          description: >-
            Load on the vehicle (for shipments: carried between pickup and
            delivery).


            On a [`Shipment`] this is the **symmetric** form — one quantity
            picked

            up and the same quantity dropped off. For a cargo swap whose two
            legs

            move different amounts, set [`Stop::demand`] on both legs instead;

            combining the two forms is rejected with HTTP 400.
        drop_fee:
          type:
            - number
            - 'null'
          format: double
          description: >-
            Drop **price** — what leaving this task unserved costs you, in the
            same

            currency as `objective.costs`, and the only way to make a task

            droppable.


            This is money, not a priority: it enters the same cost the solver

            minimises, so the task is served only while serving it costs less
            than

            the fee. Fees the plan chose to pay are reported in

            `estimated_cost.components.drop_fees` and summed into `total`.


            Rejected with HTTP 400 alongside `mandatory: true` (a task that
            cannot

            drop has no drop price). On a shipment the fee applies to the pair,

            charged once.


            Replaces the removed `unassigned_cost` (a lexicographic
            drop-*ordering*

            tier that never traded against travel cost, so no number a caller
            set

            there could ever price a drop). A request still sending it gets an

            `unknown field` rejection.
          example: 25
        eligible_vehicles:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/Eligibility'
              description: Hard vehicle allow/exclude.
          description: Hard vehicle allow/exclude.
        id:
          type: string
          description: Unique job id (must be unique across `jobs` + `shipments`).
          example: job-1
        location:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/Place'
              description: >-
                Where this visit happens. Omit for a "virtual" (location-less)
                job:

                it still has service duration, time windows, demand, and skills,
                but

                requires no travel — the vehicle's position doesn't change when
                it

                serves one. Virtual and located jobs may be mixed freely in the
                same

                request and route. A virtual job's stop in the solution response

                reports the resolved location of its nearest real (located)

                neighbor in the route — the predecessor when there is one,
                otherwise

                the successor — so it is `null` only on a route that has no
                located

                stop at all (an all-virtual route on an open-start/open-end
                shift).

                Shipment legs (`pickup`/`delivery`) still require a `location`.
          description: |-
            Where this visit happens. Omit for a "virtual" (location-less) job:
            it still has service duration, time windows, demand, and skills, but
            requires no travel — the vehicle's position doesn't change when it
            serves one. Virtual and located jobs may be mixed freely in the same
            request and route. A virtual job's stop in the solution response
            reports the resolved location of its nearest real (located)
            neighbor in the route — the predecessor when there is one, otherwise
            the successor — so it is `null` only on a route that has no located
            stop at all (an all-virtual route on an open-start/open-end shift).
            Shipment legs (`pickup`/`delivery`) still require a `location`.
        locked_vehicle:
          type:
            - string
            - 'null'
          description: >-
            Hard-pin this job to one vehicle id — only that vehicle may serve
            it,

            and re-optimization never moves it. Stronger than
            `eligible_vehicles`

            (which filters a set) and hard, unlike `preferences`. An id that

            matches no vehicle is rejected with HTTP 400.
          example: vehicle-1
        mandatory:
          type:
            - boolean
            - 'null'
          description: |-
            `true` = must serve or infeasible. **Default (omitted) is also
            must-serve** — a task is required unless it prices its own drop with
            [`JobCommon::drop_fee`].

            `false` is only meaningful together with `drop_fee`, and is rejected
            with HTTP 400 without it: an optional task with no price would be a
            task the solver may abandon for free.
          example: false
        preferences:
          type:
            - array
            - 'null'
          items:
            $ref: '#/components/schemas/Preference'
          description: >-
            Soft vehicle affinity preferences, priced. Each entry is
            independent:

            a job listing several preferred vehicles it doesn't get pays each

            one's `violation_cost` (compounding, the same rule as a vehicle's

            `preferred_tags`).
        preferred_date:
          type:
            - string
            - 'null'
          format: date-time
          description: >-
            Soft preference for the calendar day this task is served on, in a

            multi-day (multi-shift) problem.


            An RFC 3339 instant on the intended day — the value is matched
            against

            each shift's **UTC calendar day**, so passing the target shift's
            `from`

            verbatim is always correct. It is an instant rather than a bare date

            precisely because a date alone has no offset: `"2026-08-04"` sent by
            a

            caller in `+02:00` would silently miss a shift starting

            `2026-08-04T00:30:00+02:00`, whose UTC day is Aug 3.


            Soft, and priced by `objective.non_preferred_date_cost` — a flat
            charge

            applied once to a task served on any other day. **With that cost
            unset or

            zero this field has no effect.** The charge sits in the tier-3 money

            cost, below the drop penalty, so a task is never left unassigned to

            honour its preferred date; `locked_vehicle` (hard) also still wins.


            Supported on [`Shipment`]s as well as visits. Both legs of a
            shipment

            ride the same route, so the preferred day is a property of the pair:
            set

            it on the shipment itself and the solver stamps the pickup leg,
            charging

            `non_preferred_date_cost` once for the pair rather than once per
            leg.
          example: '2026-08-04T08:00:00+02:00'
        service_duration_s:
          type:
            - integer
            - 'null'
          format: int32
          description: |-
            On-site service time paid per job (seconds).

            This is the nominal figure. The serving vehicle's `service_factor`
            scales it — the time actually scheduled is
            `service_duration_s / service_factor` — so the solution's
            `stops[].service_s` may differ from what you sent here.
          example: 300
        setup_duration_s:
          type:
            - integer
            - 'null'
          format: int32
          description: |-
            Setup time paid once on arrival at a new location (seconds),
            distinct from `service_duration_s`. Skipped when the immediate real
            predecessor in the route already sits at this location — co-located
            stops share one setup.

            Not scaled by the vehicle's `service_factor`: setup is fixed arrival
            overhead (parking, walking to the door), not the work itself.
          example: 60
        skills:
          type:
            - array
            - 'null'
          items:
            $ref: '#/components/schemas/SkillReq'
          description: >-
            Hard skill requirements — the assigned vehicle must provide every
            one.
        tags:
          type:
            - array
            - 'null'
          items:
            type: string
          description: >-
            Free-form task labels (zones/territories, grouping/relation labels).

            Used for `group`/relation resolution and, when a shift scopes itself
            via

            `serves_tags` (hard ANY-overlap) or a vehicle prices them via

            `preferred_tags` (soft), for tag matching. ANY-overlap — contrast
            with

            `skills`, which requires ALL. Not capability matching on its own.
        target_arrival:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/TargetArrival'
              description: >-
                Soft target arrival for this task.

                **Not yet supported on [`Shipment`]s (Phase 1)** — rejected with

                HTTP 400 if set there; will apply to the delivery leg only once

                promoted (the old pivot DTO has no slot for it on either
                shipment leg).
          description: >-
            Soft target arrival for this task.

            **Not yet supported on [`Shipment`]s (Phase 1)** — rejected with

            HTTP 400 if set there; will apply to the delivery leg only once

            promoted (the old pivot DTO has no slot for it on either shipment
            leg).
        time_windows:
          type:
            - array
            - 'null'
          items:
            $ref: '#/components/schemas/TimeWindow'
          description: Time windows for this visit.
      example:
        demand:
          weight: 20
        id: job-1
        location:
          coordinate:
            - 3.725
            - 51.05
        service_duration_s: 300
        skills:
          - name: fridge
        time_windows:
          - from: '2026-07-01T09:00:00+02:00'
            to: '2026-07-01T12:00:00+02:00'
    Objective:
      type: object
      description: >-
        Top-level objective configuration.


        The objective order is fixed and lexicographic, with exactly two tiers

        (`Objective::key` in `solver-core`): **serve required tasks** (the
        summed

        `unassignment_penalty` of the unassigned) ≫ **minimize cost** (the
        weighted

        `costs` sum, including the `drop_fee` of every unassigned priced task).

        There is no vehicle-count tier: `minimize_vehicles` is a *price*, not a

        rank — see ADR-0012. It replaced an ordered `priorities` token list that

        could not actually be reordered (issue #588).
      properties:
        costs:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/Costs'
              description: Monetary cost knobs.
          description: Monetary cost knobs.
        grouping:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/Grouping'
              description: Job-proximity cluster cohesion. See [`Grouping`].
          description: Job-proximity cluster cohesion. See [`Grouping`].
        minimize_vehicles:
          type:
            - boolean
            - 'null'
          description: |-
            Whether the fleet-wide per-vehicle charge applies. Default `true`.

            This does **not** add an objective tier. `false` zeroes
            `cost_per_vehicle`; `true` leaves it as configured — and since it
            defaults to 0 (ADR-0012), `true` and `false` are behaviourally
            identical unless something prices a vehicle. Setting this to `true`
            *and* an explicit `costs.per_vehicle` is rejected with a 400; a
            per-vehicle `cost_model` is not, and overrides `false`.
          example: true
        non_preferred_date_cost:
          type:
            - number
            - 'null'
          format: double
          description: >-
            Flat charge for serving a task on a day other than its
            `preferred_date`.


            Applied once per task whose `preferred_date` resolves to a different
            UTC

            calendar day than the shift serving it; tasks without a
            `preferred_date`

            are never charged. Unset or `0.0` disables the preference entirely,

            which also makes every `preferred_date` in the request inert.


            Sits in the tier-3 money cost, so it trades off against travel and

            vehicle cost but never against serving a task at all.
          example: 50
          minimum: 0
      additionalProperties: false
      example:
        costs:
          currency: EUR
          per_travel_hour: 3600
        minimize_vehicles: true
    Options:
      type: object
      description: >-
        Top-level options block.


        (`output.arrival_snap_s` was removed (issue #588) — a silently-ignored

        cosmetic knob; arrival rounding is a client-side concern. `async`/

        `webhook_url` were removed with it: the solver-gateway owns the async

        lifecycle. `output.polylines` and a caller-supplied `runtime.matrix`
        were

        later removed as Phase-1 stubs that never shipped (#889); the `solve`
        CLI

        keeps an offline inline-matrix escape hatch outside the schema.)
      properties:
        runtime:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/RuntimeOptions'
              description: Runtime solver options.
          description: Runtime solver options.
      additionalProperties: false
      example:
        runtime:
          seed: 42
          time_limit_s: 5
    Relation:
      oneOf:
        - type: object
          title: Ordered
          description: Ordering (gaps allowed).
          required:
            - type
          properties:
            groups:
              type:
                - array
                - 'null'
              items:
                type: string
              description: >-
                Ordered list of tag labels declaring cross-group precedence: on

                every route, all members of `groups[0]` precede all members of

                `groups[1]`, and so on. Tasks carry membership via `tags`.

                Mutually exclusive with `job_ids`. Requires `>= 2` entries.


                Visits only: if a tag named here selects a **shipment**, the
                request

                is rejected with HTTP 400 (a shipment's two legs already carry
                their

                own hard precedence, which cross-group ordering cannot compose
                with

                yet). Name the shipment in `job_ids` instead, or drop the tag
                from it.
              minItems: 2
            job_ids:
              type:
                - array
                - 'null'
              items:
                type: string
              description: >-
                Explicit task ids in the relation (mutually exclusive with
                `groups`).
            type:
              type: string
              enum:
                - ordered
        - type: object
          title: Same resource
          description: Same resource (same vehicle type/driver across routes).
          required:
            - type
          properties:
            group:
              type:
                - string
                - 'null'
              description: >-
                Tag selecting the relation members (mutually exclusive with
                `job_ids`).
            job_ids:
              type:
                - array
                - 'null'
              items:
                type: string
              description: >-
                Explicit task ids in the relation (mutually exclusive with
                `group`).
            type:
              type: string
              enum:
                - same_resource
        - type: object
          title: Same day
          description: >-
            Same calendar day (`Vehicle::day`). Hard-only: constrains the days
            of

            members that end up assigned, mirroring `same_resource`'s

            partial-service semantics.
          required:
            - type
          properties:
            group:
              type:
                - string
                - 'null'
              description: >-
                Tag selecting the relation members (mutually exclusive with
                `job_ids`).
            job_ids:
              type:
                - array
                - 'null'
              items:
                type: string
              description: >-
                Explicit task ids in the relation (mutually exclusive with
                `group`).
            type:
              type: string
              enum:
                - same_day
        - type: object
          title: Synchronized
          description: Synchronized starts across vehicles.
          required:
            - tasks
            - type
          properties:
            max_wait_s:
              type:
                - integer
                - 'null'
              format: int64
              description: Maximum wait allowed between synchronized starts in seconds.
            tasks:
              type: array
              items:
                $ref: '#/components/schemas/SyncParticipant'
              description: Participating tasks.
            type:
              type: string
              enum:
                - synchronized
      description: >-
        A hard constraint between tasks. Tagged union; each variant carries only

        its valid fields.


        All four variants are wired: `ordered` (via `job_ids` or `groups`),

        `same_resource`, `same_day`, and `synchronized`. Member selection is

        **either** explicit `job_ids` **or** a `group` tag (`same_resource`/

        `same_day`; validated not-both) — `ordered` uses `job_ids` or the

        cross-group `groups` list.


        (The never-wired `consecutive` and `same_route` variants, the
        per-relation

        `id` and soft `violation_cost` fields, `ordered`'s
        `group`/`min_interval_s`/

        `max_interval_s`/`measure_from`, and `synchronized.tasks[*].vehicles`
        were

        removed: every one was rejected with HTTP 400 on any value. Each can
        return

        as a new field/variant once its core primitive exists — relations are

        hard-only until then.)
      example:
        job_ids:
          - job-1
          - job-2
        type: ordered
    Shipment:
      type: object
      description: >-
        A bound pickup→delivery shipment (the `shipments[]` entry type).


        Two stops served by one vehicle, pickup strictly before delivery, with

        `demand` carried on the vehicle between the two legs.


        `mandatory`/`drop_fee` and vehicle eligibility (`eligible_vehicles`,
        allow

        **and** exclude, any length) are fully supported and applied to both
        legs — a

        shipment's pickup and delivery share one droppability/eligibility
        decision,

        since one vehicle must serve both.
      required:
        - id
        - pickup
        - delivery
      properties:
        delivery:
          $ref: '#/components/schemas/Stop'
          description: Delivery leg (load off).
        demand:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/BTreeMap'
              description: >-
                Load on the vehicle (for shipments: carried between pickup and
                delivery).


                On a [`Shipment`] this is the **symmetric** form — one quantity
                picked

                up and the same quantity dropped off. For a cargo swap whose two
                legs

                move different amounts, set [`Stop::demand`] on both legs
                instead;

                combining the two forms is rejected with HTTP 400.
          description: >-
            Load on the vehicle (for shipments: carried between pickup and
            delivery).


            On a [`Shipment`] this is the **symmetric** form — one quantity
            picked

            up and the same quantity dropped off. For a cargo swap whose two
            legs

            move different amounts, set [`Stop::demand`] on both legs instead;

            combining the two forms is rejected with HTTP 400.
        drop_fee:
          type:
            - number
            - 'null'
          format: double
          description: >-
            Drop **price** — what leaving this task unserved costs you, in the
            same

            currency as `objective.costs`, and the only way to make a task

            droppable.


            This is money, not a priority: it enters the same cost the solver

            minimises, so the task is served only while serving it costs less
            than

            the fee. Fees the plan chose to pay are reported in

            `estimated_cost.components.drop_fees` and summed into `total`.


            Rejected with HTTP 400 alongside `mandatory: true` (a task that
            cannot

            drop has no drop price). On a shipment the fee applies to the pair,

            charged once.


            Replaces the removed `unassigned_cost` (a lexicographic
            drop-*ordering*

            tier that never traded against travel cost, so no number a caller
            set

            there could ever price a drop). A request still sending it gets an

            `unknown field` rejection.
          example: 25
        eligible_vehicles:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/Eligibility'
              description: Hard vehicle allow/exclude.
          description: Hard vehicle allow/exclude.
        id:
          type: string
          description: Unique job id (must be unique across `jobs` + `shipments`).
          example: ship-1
        locked_vehicle:
          type:
            - string
            - 'null'
          description: >-
            Hard-pin this job to one vehicle id — only that vehicle may serve
            it,

            and re-optimization never moves it. Stronger than
            `eligible_vehicles`

            (which filters a set) and hard, unlike `preferences`. An id that

            matches no vehicle is rejected with HTTP 400.
          example: vehicle-1
        mandatory:
          type:
            - boolean
            - 'null'
          description: |-
            `true` = must serve or infeasible. **Default (omitted) is also
            must-serve** — a task is required unless it prices its own drop with
            [`JobCommon::drop_fee`].

            `false` is only meaningful together with `drop_fee`, and is rejected
            with HTTP 400 without it: an optional task with no price would be a
            task the solver may abandon for free.
          example: false
        pickup:
          $ref: '#/components/schemas/Stop'
          description: Pickup leg (load on).
        preferences:
          type:
            - array
            - 'null'
          items:
            $ref: '#/components/schemas/Preference'
          description: >-
            Soft vehicle affinity preferences, priced. Each entry is
            independent:

            a job listing several preferred vehicles it doesn't get pays each

            one's `violation_cost` (compounding, the same rule as a vehicle's

            `preferred_tags`).
        preferred_date:
          type:
            - string
            - 'null'
          format: date-time
          description: >-
            Soft preference for the calendar day this task is served on, in a

            multi-day (multi-shift) problem.


            An RFC 3339 instant on the intended day — the value is matched
            against

            each shift's **UTC calendar day**, so passing the target shift's
            `from`

            verbatim is always correct. It is an instant rather than a bare date

            precisely because a date alone has no offset: `"2026-08-04"` sent by
            a

            caller in `+02:00` would silently miss a shift starting

            `2026-08-04T00:30:00+02:00`, whose UTC day is Aug 3.


            Soft, and priced by `objective.non_preferred_date_cost` — a flat
            charge

            applied once to a task served on any other day. **With that cost
            unset or

            zero this field has no effect.** The charge sits in the tier-3 money

            cost, below the drop penalty, so a task is never left unassigned to

            honour its preferred date; `locked_vehicle` (hard) also still wins.


            Supported on [`Shipment`]s as well as visits. Both legs of a
            shipment

            ride the same route, so the preferred day is a property of the pair:
            set

            it on the shipment itself and the solver stamps the pickup leg,
            charging

            `non_preferred_date_cost` once for the pair rather than once per
            leg.
          example: '2026-08-04T08:00:00+02:00'
        skills:
          type:
            - array
            - 'null'
          items:
            $ref: '#/components/schemas/SkillReq'
          description: >-
            Hard skill requirements — the assigned vehicle must provide every
            one.
        tags:
          type:
            - array
            - 'null'
          items:
            type: string
          description: >-
            Free-form task labels (zones/territories, grouping/relation labels).

            Used for `group`/relation resolution and, when a shift scopes itself
            via

            `serves_tags` (hard ANY-overlap) or a vehicle prices them via

            `preferred_tags` (soft), for tag matching. ANY-overlap — contrast
            with

            `skills`, which requires ALL. Not capability matching on its own.
        target_arrival:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/TargetArrival'
              description: >-
                Soft target arrival for this task.

                **Not yet supported on [`Shipment`]s (Phase 1)** — rejected with

                HTTP 400 if set there; will apply to the delivery leg only once

                promoted (the old pivot DTO has no slot for it on either
                shipment leg).
          description: >-
            Soft target arrival for this task.

            **Not yet supported on [`Shipment`]s (Phase 1)** — rejected with

            HTTP 400 if set there; will apply to the delivery leg only once

            promoted (the old pivot DTO has no slot for it on either shipment
            leg).
      example:
        delivery:
          location:
            coordinate:
              - 3.71
              - 51.06
          service_duration_s: 300
        demand:
          weight: 20
        id: ship-1
        pickup:
          location:
            coordinate:
              - 3.725
              - 51.05
          service_duration_s: 300
    Vehicle:
      type: object
      description: A vehicle type available to serve tasks.
      required:
        - id
        - shifts
      properties:
        capacity:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/BTreeMap'
              description: Named capacity dimensions.
          description: Named capacity dimensions.
        cost:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/Cost'
              description: Per-vehicle cost overrides.
          description: Per-vehicle cost overrides.
        id:
          type: string
          description: Vehicle type id; addressable. Unique across `vehicles[]`.
          example: v1
        limits:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/VehicleLimits'
              description: Hard operating limits.
          description: Hard operating limits.
        preferred_tags:
          type:
            - array
            - 'null'
          items:
            $ref: '#/components/schemas/TagPreference'
          description: >-
            Soft tag preferences (replaces `region`). Serving a task that does
            not

            carry a preferred tag incurs that entry's `violation_cost`. Entries

            apply independently: a task lacking several preferred tags incurs
            each

            cost.
        profile:
          type:
            - string
            - 'null'
          description: |-
            Routing profile: `"car"` (default), `"truck"`, or `"bike"` — selects
            which Solvice Maps routing profile computes this vehicle's distance/
            time matrix. Case-insensitive on input, any other value is rejected
            with HTTP 400.
          example: car
        service_factor:
          type:
            - number
            - 'null'
          format: double
          description: >-
            Work-rate multiplier for service time at stops (1.0 = base rate).


            Same direction as `speed_factor`: effective service time =

            `service_duration_s / service_factor`, so values > 1.0 model a
            driver who

            works FASTER and spends less time at each stop, and < 1.0 one who is

            slower. Use it for crew size, equipment, or experience — how quickly
            the

            work gets done, as distinct from how fast the vehicle drives.


            Applies to job/stop `service_duration_s` only. Deliberately NOT
            applied

            to `setup_duration_s` (fixed arrival overhead such as parking),

            `shift.depot_service_time_s` or `reloads[].service_duration_s`
            (depot-side

            handling), waiting time, or break durations.


            Must be in `(0, 5]`. Default 1.0. A shift may override it for its
            own

            day with [`Shift::service_factor`].
          maximum: 5
          exclusiveMinimum: 0
        shifts:
          type: array
          items:
            $ref: '#/components/schemas/Shift'
          description: One or more working shifts.
        skills:
          type:
            - array
            - 'null'
          items:
            type: string
          description: Capabilities the vehicle provides.
        speed_factor:
          type:
            - number
            - 'null'
          format: double
          description: >-
            Travel-speed multiplier vs. the matrix (1.0 = base speed). Effective

            travel time = `matrix_time / speed_factor`, so values > 1.0 model a

            faster vehicle and < 1.0 a slower one. Must be in `(0, 5]`. Default
            1.0.


            Covers driving only. Its companion [`Self::service_factor`] scales
            the

            work done at stops; the two are independent and share this direction
            and

            range.
          maximum: 5
          exclusiveMinimum: 0
      additionalProperties: false
      example:
        capacity:
          volume: 20
          weight: 500
        id: v1
        profile: car
        shifts:
          - end:
              coordinate:
                - 3.7174
                - 51.0543
            from: '2026-07-01T08:00:00+02:00'
            start:
              coordinate:
                - 3.7174
                - 51.0543
            to: '2026-07-01T17:00:00+02:00'
        skills:
          - fridge
    BTreeMap:
      type: object
      description: >-
        Named capacity/demand/load dimensions, e.g. `{"weight": 30, "volume":
        2}`. Keys are user-chosen dimension names; values are integer
        quantities. Order-independent and self-documenting.
      additionalProperties:
        type: integer
        format: int32
      propertyNames:
        type: string
      example:
        volume: 2
        weight: 20
    Eligibility:
      type: object
      description: 'Hard vehicle eligibility: whitelist or blacklist (mutually exclusive).'
      properties:
        allowed:
          type:
            - array
            - 'null'
          items:
            type: string
          description: Hard whitelist of vehicle ids. Omit = all vehicles allowed.
        excluded:
          type:
            - array
            - 'null'
          items:
            type: string
          description: Hard blacklist of vehicle ids. Mutually exclusive with `allowed`.
      additionalProperties: false
      example:
        allowed:
          - v1
          - v2
    Place:
      oneOf:
        - type: object
          title: Coordinate
          description: Inline coordinate `[longitude, latitude]` (GeoJSON order).
          required:
            - coordinate
          properties:
            coordinate:
              type: array
              items:
                type: number
                format: double
              description: >-
                Coordinate `[longitude, latitude]` in GeoJSON order — exactly
                two numbers.
              maxItems: 2
              minItems: 2
        - type: array
          title: Coordinate shorthand
          items:
            type: number
            format: double
          description: >-
            Bare coordinate shorthand: `[longitude, latitude]`, equivalent to `{
            "coordinate": [longitude, latitude] }`.
          maxItems: 2
          minItems: 2
      description: >-
        A place: an inline coordinate.


        Discriminated by which key is present (not a `type` tag), plus a bare

        coordinate array as shorthand:

        - `[lon, lat]`

        - `{ "coordinate": [lon, lat] }`


        `Deserialize` is hand-written (not derived) so every rejected shape
        names

        the accepted keys — see the `impl` below. `location_id` (a reference
        into a

        shared `locations[]` table) was considered and dropped: plain locations

        never carry data beyond a coordinate, and co-located jobs already dedupe

        correctly on matching inline coordinates, so the extra indirection had
        no

        payoff (GH #339). A first-class depot reference (`{ "depot": "id" }`)
        was

        also considered and dropped as a Phase-1 stub that never shipped (#889).
      example:
        coordinate:
          - 3.725
          - 51.05
    Preference:
      type: object
      description: A soft vehicle affinity preference, priced.
      required:
        - vehicle
        - violation_cost
      properties:
        vehicle:
          type: string
          description: Preferred vehicle id.
          example: v1
        violation_cost:
          type: number
          format: double
          description: Cost incurred when a different vehicle serves the job.
          example: 50
      additionalProperties: false
      example:
        vehicle: v1
        violation_cost: 50
    SkillReq:
      type: object
      description: A hard skill requirement — the assigned vehicle must provide it.
      required:
        - name
      properties:
        name:
          type: string
          description: Skill name the assigned vehicle must provide.
          example: fridge
      additionalProperties: false
      example:
        name: fridge
    TargetArrival:
      type: object
      description: >-
        Soft target arrival: arriving after `at` is allowed but priced. Distinct

        from a hard time window — a target is a point, never causes

        infeasibility, and (unlike a window's `from`) never forces the vehicle
        to

        wait when it arrives early.
      required:
        - at
        - per_late_hour
      properties:
        at:
          type: string
          format: date-time
          description: Target arrival instant (ISO 8601).
          example: '2026-07-01T10:00:00+02:00'
        per_late_hour:
          type: number
          format: double
          description: >-
            Cost per hour of arrival beyond `at` (linear, prorated per second

            internally). Must be `> 0` to have any effect; independent of

            `objective.costs.per_late_hour` (which prices *time-window*
            lateness,

            not a target point).
          example: 60
      additionalProperties: false
      example:
        at: '2026-07-01T10:00:00+02:00'
        per_late_hour: 60
    TimeWindow:
      type: object
      description: |-
        A time-window constraint. Per-side costs grade it from hard to soft:
        absent cost on a side = hard on that side.
      required:
        - from
        - to
      properties:
        cost:
          type:
            - number
            - 'null'
          format: double
          description: >-
            Optional flat cost (plain currency units) added to the objective
            when

            this job/stop is served within THIS window rather than a
            free/cheaper

            alternative window on the same task. `None` = 0 (no charge). Every

            window in the list remains individually hard — a job must still land

            in *some* window — this only grades preference among the hard

            alternatives; it does not soften time-window feasibility — every
            window

            is hard on both sides. Must be finite and `>= 0`,

            and requires at least 2 windows on the task. Once any window on the

            task carries a *nonzero* cost, that task's windows must also be

            pairwise non-overlapping (a task whose costs are all `0`/absent
            still

            merges overlapping windows as before).


            **Cannot be combined with `objective.costs.per_late_hour > 0`** — a

            nonzero window `cost` anywhere in the request alongside priced
            lateness

            is rejected with HTTP 400 (the two read opposite ways about which

            window was paid for). Choose one: priced alternative windows, or
            priced

            lateness.
          minimum: 0
        from:
          type: string
          format: date-time
          description: Earliest allowed arrival (ISO 8601 instant).
          example: '2026-07-01T09:00:00+02:00'
        to:
          type: string
          format: date-time
          description: Latest allowed arrival (ISO 8601 instant).
          example: '2026-07-01T12:00:00+02:00'
      additionalProperties: false
      example:
        cost: 15
        from: '2026-07-01T09:00:00+02:00'
        to: '2026-07-01T12:00:00+02:00'
    Costs:
      type: object
      description: Monetary cost knobs for the objective layer (fleet defaults).
      properties:
        currency:
          type:
            - string
            - 'null'
          description: Informational currency label, echoed in the response.
          example: EUR
        per_late_hour:
          type:
            - number
            - 'null'
          format: double
          description: >-
            Cost per hour of lateness. Omit for hard windows. An explicit `0.0`

            is rejected as ambiguous (free lateness vs hard windows read
            opposite

            ways — and lateness must never be free); every other `costs` rate

            treats `0.0` as "priced at zero".


            Fleet-wide only: there is no per-window lateness or earliness rate
            in

            the schema today, so this single rate prices lateness on every
            window of

            every task. Setting it `> 0` alongside a nonzero

            `TimeWindow.cost` anywhere in the request is rejected with HTTP 400
            —

            see [`TimeWindow::cost`].
          example: 60
        per_overtime_hour:
          type:
            - number
            - 'null'
          format: double
          description: >-
            Cost per hour of overtime — the span a route finishes past a shift's

            soft end (`to`, when `max_overtime_s` grants an extension).
            `0`/absent

            = overtime is unpriced. Rounded to the internal cost grid
            (resolution

            1/100 000 of a currency unit per second); a nonzero rate too small
            to

            represent is rejected.
          example: 45
        per_travel_hour:
          type:
            - number
            - 'null'
          format: double
          description: |-
            Cost per hour of travel time. Omitting `costs` entirely seeds a
            realistic travel baseline — **€35/hour and €0.20/km** — and nothing
            else: there is no fleet-wide per-vehicle default, so `per_vehicle`
            stays at `0` until you set it (or a per-vehicle `Cost.fixed`). Every
            other rate in this object also defaults to `0`/unset. Rounded to the
            internal cost grid (resolution 1/100 000 of a currency unit per
            second); a nonzero rate too small to represent is rejected.
          example: 3600
        per_travel_km:
          type:
            - number
            - 'null'
          format: double
          description: |-
            Cost per kilometre of travel distance. Rounded to the internal cost
            grid (resolution 1/100 000 of a currency unit per metre); a nonzero
            rate too small to represent is rejected.
          example: 1000
        per_vehicle:
          type:
            - number
            - 'null'
          format: double
          description: >-
            Flat activation cost per non-empty route (per vehicle used). An

            explicit value replaces count-first fleet minimization with a money

            trade-off — setting it together with `objective.minimize_vehicles:

            true` is rejected as contradictory (set `minimize_vehicles: false`
            or

            omit `per_vehicle`). A per-vehicle `Cost.fixed` overrides this

            fleet-wide default for that vehicle.
          example: 150
        per_wait_hour:
          type:
            - number
            - 'null'
          format: double
          description: |-
            Cost per hour of waiting time, applied to a route's total idle/wait
            time. Converted internally to a per-second wait cost rate.
          example: 10
      additionalProperties: false
      example:
        currency: EUR
        per_travel_hour: 3600
        per_travel_km: 1000
    Grouping:
      type: object
      description: >-
        Keep geographically clustered tasks consecutive in a route.


        Two tasks are in the same **group** when they lie within `radius_m` of

        each other. The solver adds `cost_per_reentry` to the objective each
        time

        a route leaves a group and comes back — minimising the
        "passing-waypoint"

        / area re-entry count last-mile fleets track. Presence of this object

        enables the feature; omit it to disable.
      required:
        - cost_per_reentry
      properties:
        cost_per_reentry:
          type: number
          format: double
          description: >-
            Cost added to the objective per group re-entry (currency units).

            Rejected with HTTP 400 above 1.0 currency unit (the internal grid
            caps

            the raw penalty at `CURRENCY_SCALE`); higher keeps clusters together

            more aggressively at the expense of total travel.
          example: 0.5
          maximum: 1
          minimum: 0
        distance:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/GroupingDistance'
              description: How neighbour distance is measured. Default `"straight_line"`.
          description: How neighbour distance is measured. Default `"straight_line"`.
        radius_m:
          type:
            - integer
            - 'null'
          format: int32
          description: Radius defining a group, in metres. Default `180`.
          example: 180
      additionalProperties: false
      example:
        cost_per_reentry: 0.5
        distance: straight_line
        radius_m: 180
    RuntimeOptions:
      type: object
      description: Runtime (non-goal) solver options.
      properties:
        seed:
          type:
            - integer
            - 'null'
          format: int64
          description: >-
            Random seed for the search. Repeated runs on one machine usually
            return

            an identical plan; a materially faster or slower machine can differ.
          example: 42
          minimum: 0
        time_limit_s:
          type:
            - integer
            - 'null'
          format: int32
          description: |-
            Wall-clock solve-time budget in seconds (default 1, maximum 300 —
            larger values are rejected with HTTP 400). Pure search time; matrix
            fetch and queue wait are bounded separately, not deducted here.
          example: 5
          maximum: 300
          minimum: 0
        traffic:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/TrafficOptions'
              description: Traffic-aware matrix options (departure-time override).
          description: Traffic-aware matrix options (departure-time override).
      additionalProperties: false
      example:
        seed: 42
        time_limit_s: 5
    SyncParticipant:
      type: object
      description: A participant in a `Synchronized` relation.
      required:
        - id
      properties:
        id:
          type: string
          description: Task id.
          example: job-1
      additionalProperties: false
      example:
        id: job-1
    Stop:
      type: object
      description: One leg of a [`Shipment`] (pickup or delivery).
      required:
        - location
      properties:
        break_interruptible:
          type:
            - boolean
            - 'null'
          description: >-
            **Phase 1:** setting `false` is rejected with HTTP 400 (the default
            `true` is supported).

            Whether a break may interrupt service here. Default `true`.
          example: true
        demand:
          type:
            - object
            - 'null'
          description: >-
            Per-leg load for an asymmetric shipment: how much this stop alone
            loads (pickup) or unloads (delivery), as non-negative magnitudes.
            Set on both legs or neither. See the `Stop` schema description for
            the full semantics.
          additionalProperties:
            type: integer
            format: int32
            minimum: 0
        id:
          type:
            - string
            - 'null'
          description: >-
            Stop id. Defaults to `"{shipment}:pickup"` /
            `"{shipment}:delivery"`.

            The effective id (custom or default) must be unique across all job,

            shipment, and stop ids — a collision is rejected with HTTP 400.
          example: ship-1:pickup
        location:
          $ref: '#/components/schemas/Place'
          description: Where this stop happens.
        service_duration_s:
          type:
            - integer
            - 'null'
          format: int32
          description: |-
            On-site service time paid per job (seconds).

            This is the nominal figure. The serving vehicle's `service_factor`
            scales it — the time actually scheduled is
            `service_duration_s / service_factor` — so the solution's
            `stops[].service_s` may differ from what you sent here.
          example: 300
        setup_duration_s:
          type:
            - integer
            - 'null'
          format: int32
          description: |-
            Setup time paid once on arrival at a new location (seconds),
            distinct from `service_duration_s`. Skipped when the immediate real
            predecessor in the route already sits at this location — co-located
            stops share one setup.

            Not scaled by the vehicle's `service_factor`: setup is fixed arrival
            overhead (parking, walking to the door), not the work itself.
          example: 60
        time_windows:
          type:
            - array
            - 'null'
          items:
            $ref: '#/components/schemas/TimeWindow'
          description: Time windows for this stop.
      additionalProperties: false
      example:
        id: ship-1:pickup
        location:
          coordinate:
            - 3.725
            - 51.05
        service_duration_s: 300
        setup_duration_s: 60
        time_windows:
          - from: '2026-07-01T09:00:00+02:00'
            to: '2026-07-01T12:00:00+02:00'
    Cost:
      type: object
      description: >-
        **Phase 1:** at most one of `fixed`, `per_travel_hour`, `per_stop` may
        be

        set (only these three fields are live today — see field docs below for

        the rest). Per-vehicle cost overrides; `objective.costs` are the fleet

        defaults.
      properties:
        fixed:
          type:
            - number
            - 'null'
          format: double
          description: One-time cost when the vehicle is used.
          example: 100
        per_stop:
          type:
            - number
            - 'null'
          format: double
          description: Cost per completed stop.
          example: 1
        per_travel_hour:
          type:
            - number
            - 'null'
          format: double
          description: Cost per hour of travel time.
          example: 30
        per_travel_km:
          type:
            - number
            - 'null'
          format: double
          description: >-
            Cost per kilometre driven, for THIS vehicle. Overrides the
            fleet-wide

            `objective.costs.per_travel_km` rate. Combinable with the
            single-term

            billing fields (`fixed`/`per_stop`/`per_travel_hour`) — a dispatch

            charge, a wage, and a fuel rate are independent components. `0.0`

            makes distance free for this vehicle; absent inherits the fleet
            rate.
          example: 0.4
          minimum: 0
      additionalProperties: false
      example:
        per_travel_hour: 30
    VehicleLimits:
      type: object
      description: Vehicle distance/time limits.
      properties:
        max_distance_m:
          type:
            - integer
            - 'null'
          format: int64
          description: Hard maximum distance in metres.
          example: 200000
        max_route_duration_s:
          type:
            - integer
            - 'null'
          format: int64
          description: >-
            Hard maximum route duration in seconds: the whole-route span from
            shift

            departure to final arrival — travel, service, and wait all count.

            (Renamed from `max_duty_time_s` — the cap measures route span,

            not the regulatory duty-time definition; see issue #588.)
          example: 32400
      additionalProperties: false
      example:
        max_distance_m: 200000
        max_route_duration_s: 32400
    TagPreference:
      type: object
      description: A soft tag preference, priced (replaces `region`).
      required:
        - tag
        - violation_cost
      properties:
        tag:
          type: string
          description: Preferred tag.
          example: north
        violation_cost:
          type: number
          format: double
          description: Cost incurred when a task without this tag is served.
          example: 25
      additionalProperties: false
      example:
        tag: north
        violation_cost: 25
    Shift:
      type: object
      description: One working shift for a vehicle.
      required:
        - from
        - to
      properties:
        breaks:
          type:
            - array
            - 'null'
          items:
            $ref: '#/components/schemas/Break'
          description: |-
            Scheduled driver breaks within this shift — see [`Break`] for the
            supported subset. Injected breaks appear as `break` stops in the
            response routes.
        depot_service_time_s:
          type:
            - integer
            - 'null'
          format: int32
          description: >-
            Depot service (handling) time in seconds, applied symmetrically at
            both

            depot ends of the shift: at the start depot it is a load that delays

            departure, at the end depot an unload that extends route duration.
            Only

            applied to closed depot ends (an open end has no depot to service).

            Default 0.


            A single symmetric value (load = unload) matches the data available

            today — separate load/unload durations would be a future additive

            field. This time is applied whenever the shift has a closed end;
            there

            is no separate flag to force the end-depot visit.


            Not scaled by [`Vehicle::service_factor`]. That factor is the
            driver's

            on-site work rate and applies to task `service_duration_s` only;
            depot

            load/unload stays nominal however fast the driver works.
        end:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/Place'
              description: End place. Absent = open end.
          description: End place. Absent = open end.
        from:
          type: string
          format: date-time
          description: Shift start (ISO 8601 instant).
          example: '2026-07-01T08:00:00+02:00'
        ignore_end_leg:
          type:
            - boolean
            - 'null'
          description: Symmetric to `ignore_start_leg` for the last-task → `end` leg.
          example: false
        ignore_start_leg:
          type:
            - boolean
            - 'null'
          description: |-
            When `true`, the `start` → first-task leg costs nothing (not counted
            in objective, feasibility timing, or total distance/duration) — the
            depot is still known and reported (a real stop, snapped coordinate,
            departure time). Distinct from omitting `start` (a fully open route,
            which drops the depot entirely): this keeps the depot visible while
            making the leg to it free. Requires `start`; a no-op when `start` is
            absent. Default `false`.
          example: false
        initial_load:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/BTreeMap'
              description: >-
                Load already on the vehicle at shift start (pre-loaded), per

                dimension. Occupies capacity on the first trip only — flushed at
                the

                first reload — so it reduces first-trip room per dimension,
                enforced

                by the per-trip capacity constraint. Not validated against

                `capacity` at request time: a value at or beyond capacity is

                accepted and surfaces as a solve-time result (jobs left
                unassigned,

                or deferred until the vehicle's first reload flushes it), not an

                HTTP 400. A dimension named here but absent from `capacity`
                behaves

                the same as capacity `0` in that dimension.


                Also the place to declare stock for an asymmetric shipment whose

                delivery leg exceeds its pickup (see [`Stop::demand`]): those
                goods must

                already be aboard at shift start. Nothing checks that — capacity
                is an

                upper bound only, so an uncovered shortfall simply drives the
                running

                load negative instead of failing.
          description: >-
            Load already on the vehicle at shift start (pre-loaded), per

            dimension. Occupies capacity on the first trip only — flushed at the

            first reload — so it reduces first-trip room per dimension, enforced

            by the per-trip capacity constraint. Not validated against

            `capacity` at request time: a value at or beyond capacity is

            accepted and surfaces as a solve-time result (jobs left unassigned,

            or deferred until the vehicle's first reload flushes it), not an

            HTTP 400. A dimension named here but absent from `capacity` behaves

            the same as capacity `0` in that dimension.


            Also the place to declare stock for an asymmetric shipment whose

            delivery leg exceeds its pickup (see [`Stop::demand`]): those goods
            must

            already be aboard at shift start. Nothing checks that — capacity is
            an

            upper bound only, so an uncovered shortfall simply drives the
            running

            load negative instead of failing.
        max_overtime_s:
          type:
            - integer
            - 'null'
          format: int32
          description: Maximum allowed overtime beyond `to` in seconds.
          example: 1800
        max_tasks:
          type:
            - integer
            - 'null'
          format: int32
          description: Max tasks in this shift.
          example: 50
          minimum: 0
        max_trips:
          type:
            - integer
            - 'null'
          format: int32
          description: >-
            Maximum trips (depot legs) in this shift, across all reload depots.

            Counts trips, not reloads: `trips = reloads_used + 1`. `max_trips:
            1`

            means a single trip — reloads are permitted in `reloads` but the cap

            prevents ever using one. `reloads` present with `max_trips` absent

            means unlimited trips. A single reload depot can only be

            revisited up to 8 times per vehicle regardless of `max_trips`

            (`K_MAX_RELOAD_SLOTS`); a `max_trips` beyond that against one depot

            is accepted but the extra headroom is unreachable — list more than

            one reload depot to raise the practical ceiling.
          example: 2
          minimum: 0
        reloads:
          type:
            - array
            - 'null'
          items:
            $ref: '#/components/schemas/Reload'
          description: >-
            Mid-shift reload stops (multi-trip): the vehicle may return to one
            of

            these to replenish capacity and start a fresh trip. All entries in
            one

            shift must share the same `service_duration_s` (HTTP 400 if they

            differ); cap the total number of trips with `max_trips`.
        serves_tags:
          type:
            - array
            - 'null'
          items:
            type: string
          description: >-
            Hard tag scope (was shift `tags`): when set (non-empty), this shift

            serves only tasks carrying at least one of these tags. Untagged
            tasks

            remain serveable by every shift. Absent or empty = serves
            everything.
        service_factor:
          type:
            - number
            - 'null'
          format: double
          description: >-
            Work-rate multiplier for this shift only, overriding

            [`Vehicle::service_factor`] when set (absent = inherit the vehicle's

            value, which itself defaults to 1.0).


            Same meaning, direction, and `(0, 5]` range as the vehicle-level
            field:

            effective service time = `service_duration_s / service_factor`. Use
            it

            when the same driver works at a different rate on different days — a

            measured per-day efficiency, a trainee shift, a two-person crew on

            Fridays — instead of collapsing those days into one vehicle-level
            mean.


            Scope is identical to the vehicle-level field: task

            `service_duration_s` only, never `setup_duration_s`,

            `depot_service_time_s`, `reloads[].service_duration_s`, waiting, or

            breaks.
          maximum: 5
          exclusiveMinimum: 0
        start:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/Place'
              description: Start place. Absent = open start.
          description: Start place. Absent = open start.
        to:
          type: string
          format: date-time
          description: Shift end (ISO 8601 instant; hard unless `max_overtime_s` set).
          example: '2026-07-01T17:00:00+02:00'
      additionalProperties: false
      example:
        end:
          coordinate:
            - 3.7174
            - 51.0543
        from: '2026-07-01T08:00:00+02:00'
        start:
          coordinate:
            - 3.7174
            - 51.0543
        to: '2026-07-01T17:00:00+02:00'
    GroupingDistance:
      type: string
      description: How neighbour distance is measured for [`Grouping`].
      enum:
        - straight_line
        - road
      example: straight_line
    TrafficOptions:
      type: object
      description: >-
        Traffic-aware matrix options.


        Distance/duration matrices are always traffic-aware (historical-typical

        traffic); by default travel times reflect traffic at the moment the

        request is processed. Set `departure_time` to plan against another

        instant — e.g. tomorrow's 08:00 routes with morning-rush travel times,
        or

        a replay of a past run under its original conditions.
      properties:
        departure_time:
          type:
            - string
            - 'null'
          format: date-time
          description: >-
            Departure time for the traffic lookup (RFC 3339 instant, any
            offset).

            Omitted → traffic at solve time. An explicit departure time bypasses

            the server-side matrix cache.
          example: '2026-07-01T08:00:00+02:00'
      additionalProperties: false
      example:
        departure_time: '2026-07-01T08:00:00+02:00'
    Break:
      oneOf:
        - type: object
          title: Floating
          description: Flexible break taken within a window / after a trigger.
          required:
            - duration_s
            - type
          properties:
            duration_s:
              type: integer
              format: int32
              description: Break duration in seconds.
            location:
              oneOf:
                - type: 'null'
                - $ref: '#/components/schemas/Place'
                  description: >-
                    Optional pinned break location (windowed breaks only, Phase
                    1).
              description: Optional pinned break location (windowed breaks only, Phase 1).
            trigger:
              oneOf:
                - type: 'null'
                - $ref: '#/components/schemas/BreakTrigger'
                  description: >-
                    Drive/duty accumulator trigger; mutually exclusive with
                    `windows`.
              description: >-
                Drive/duty accumulator trigger; mutually exclusive with
                `windows`.
            type:
              type: string
              enum:
                - floating
            windows:
              type:
                - array
                - 'null'
              items:
                $ref: '#/components/schemas/TimeWindow'
              description: |-
                Windows bounding the break's **start** (earliest/latest start).
                Phase 1: exactly one window; mutually exclusive with `trigger`.
        - type: object
          title: Fixed
          description: |-
            Mandatory off-duty interval at a fixed time. Route activity is
            scheduled entirely before `from` or entirely after `to`.
          required:
            - from
            - to
            - type
          properties:
            from:
              type: string
              format: date-time
              description: Start of the break (ISO 8601 instant).
            to:
              type: string
              format: date-time
              description: End of the break (ISO 8601 instant).
            type:
              type: string
              enum:
                - fixed
      description: >-
        A scheduled break within a shift. Tagged by `type`.


        Supported: a `floating` break with **either** a single `windows` entry

        (a windowed break — the window bounds the break's **start**) **or** a

        `trigger` (drive/duty accumulation, the EU-561 mechanism), and a `fixed`

        hard off-duty interval. Breaks are injected between stops and appear in

        the response as `break` stops.


        **Not yet supported (Phase 1)** — rejected with HTTP 400: multiple

        `windows` entries, a `location` on triggered or fixed breaks, `windows`
        and

        `trigger` together, *neither* `windows` nor `trigger` on a `floating`
        break

        (an unconstrained break has nowhere to go), and

        `trigger.resets_accumulator: false`.


        There is no paid/unpaid cost accounting: a break's duration is never

        separately priced, and no `paid` field exists on this schema — sending
        one

        is rejected as an unknown field (HTTP 422), like any other extra key.
      example:
        duration_s: 1800
        trigger:
          after: drive
          threshold_s: 14400
        type: floating
    Reload:
      type: object
      description: |-
        A mid-shift reload stop: the vehicle returns here to replenish capacity
        and begin a fresh trip (multi-trip). See [`Shift::reloads`].
      required:
        - location
      properties:
        cost_per_visit:
          type:
            - number
            - 'null'
          format: double
          description: >-
            Monetary cost charged once per reload visit, in the objective's

            currency. Default `0` (a reload costs only its travel and whatever

            wall-clock it adds).


            Set this when a reload has a real per-visit price the travel terms

            cannot see — typically the driver labour for the unload itself

            (`service_duration_s` × the hourly rate). Without it, a reload that

            consumes only *slack* is free in the objective: it adds no route

            duration, so the solver will happily insert one to displace time
            that

            `objective.costs.per_wait_hour` would otherwise charge as idle,
            parking

            the vehicle at a depot for no capacity benefit.


            Like [`Reload::service_duration_s`], all reloads in one shift must

            currently share the same value (or all omit it); differing per-depot

            values are rejected with HTTP 400.
          example: 38.66
          minimum: 0
        location:
          $ref: '#/components/schemas/Place'
          description: >-
            Where the vehicle reloads — the shared [`Place`] shape: an inline

            `{ "coordinate": [lon, lat] }` (or bare `[lon, lat]` shorthand),
            exactly

            like `Shift.start`/`Shift.end`.
        service_duration_s:
          type:
            - integer
            - 'null'
          format: int32
          description: >-
            Handling time in seconds at this reload stop. Default 0. All reloads

            in one shift must currently share the same value (or all omit it);

            differing per-depot values are rejected with HTTP 400 — the internal

            pipeline only carries one flat reload-service-time per shift today.


            Not scaled by [`Vehicle::service_factor`]. That factor is the
            driver's

            on-site work rate and applies to task `service_duration_s` only;

            reloading is depot-side handling, so it stays nominal however fast
            the

            driver works.
          example: 300
      additionalProperties: false
      example:
        location:
          coordinate:
            - 3.725
            - 51.05
        service_duration_s: 300
    BreakTrigger:
      type: object
      description: A drive/duty accumulator trigger for a floating break.
      required:
        - after
        - threshold_s
      properties:
        after:
          $ref: '#/components/schemas/TriggerAfter'
          description: 'What to accumulate: drive time or duty time.'
        deduct_previous_breaks:
          type:
            - boolean
            - 'null'
          description: |-
            V2 compatibility — when `true`, break time already taken earlier in
            the shift counts toward this trigger's threshold (same-edge only).
            Prefer modelling each accumulation window as its own trigger.
          deprecated: true
          example: false
        resets_accumulator:
          type:
            - boolean
            - 'null'
          description: >-
            Whether the accumulator resets after the break. Default `true` (the

            EU-561 semantics; the accumulation window restarts after a
            qualifying

            break). **`false` is rejected with HTTP 400** — a non-resetting

            accumulator is not supported.
          example: true
        threshold_s:
          type: integer
          format: int64
          description: |-
            Trigger threshold in seconds (strict `>` — the break fires when the
            accumulator exceeds this). EU 561/2006 drive rule: `16200` (4 h 30).
          example: 14400
      additionalProperties: false
      example:
        after: drive
        resets_accumulator: true
        threshold_s: 14400
    TriggerAfter:
      type: string
      description: What a break trigger accumulates.
      enum:
        - drive
        - duty
      example: drive
  securitySchemes:
    apiKey:
      type: apiKey
      in: header
      name: Authorization

````