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

# Submit a run

> Submit a routing problem to solve in the background

<Info>
  **V3 is in beta.** General availability (GA) is planned for January 2027.
</Info>


## OpenAPI

````yaml vrp-v3-runs POST /v3/routing/runs
openapi: 3.1.0
info:
  title: 'Solvice Routing API: async runs'
  description: >-
    Run a routing problem in the background: submit it, poll the run, then fetch
    the plan. The run takes the same request body as `POST /v3/routing/solve`
    and its result is the same `SolveResponse`. Served by the API gateway at
    `https://api.solvice.io`, not by the solver.
  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: Runs
    description: Async routing runs
paths:
  /v3/routing/runs:
    post:
      tags:
        - Runs
      summary: Submit an async run
      description: >-
        Starts a solve in the background and returns at once with `202` and the
        run. The body is the same as `POST /v3/routing/solve`, except that
        `options.time_limit_s` may go up to 3600 and `callback` is not supported
        yet. The request is checked before the run is accepted, so a bad body is
        a `400` here, not a failed run later.


        Poll `GET /v3/routing/runs/{id}` until `status` is `succeeded`, `failed`
        or `cancelled`, then read the plan from `GET
        /v3/routing/runs/{id}/result`. At most 10 runs per account are queued or
        running at once.
      operationId: submitRoutingRun
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SolveRequest'
      responses:
        '202':
          description: Run accepted. It is `queued` until a worker starts it.
          headers:
            x-request-id:
              schema:
                type: string
              description: The request's `X-Request-Id`, echoed, or a generated `req_…` id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Run'
              example:
                id: d28568b1-34cc-4f89-b10b-dc24b26a8275
                status: queued
                links:
                  self: /v3/routing/runs/d28568b1-34cc-4f89-b10b-dc24b26a8275
                  result: /v3/routing/runs/d28568b1-34cc-4f89-b10b-dc24b26a8275/result
        '400':
          description: >-
            The request is invalid: the same `ErrorBody` as `POST
            /v3/routing/solve`. A `callback` field is `not_supported` at
            `/callback`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorBody'
        '401':
          description: >-
            Missing or invalid API key. Returned by the API gateway, so it does
            not use the `ErrorBody` shape and can have an empty body. Branch on
            the HTTP status.
        '403':
          description: >-
            The request uses a feature this account does not have (`code:
            feature_not_enabled`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorBody'
        '413':
          description: 'Request body larger than 10 MiB (`code: payload_too_large`).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorBody'
        '415':
          description: >-
            Request `Content-Type` is not `application/json` (`code:
            unsupported_media_type`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorBody'
        '429':
          description: >-
            Rate limited, or this account already has 10 queued or running runs
            (a cancelled run still counts until its worker stops). Returned by
            the API gateway, so it does not use the `ErrorBody` shape. Wait for
            the `Retry-After` header, then retry.
          headers:
            Retry-After:
              schema:
                type: integer
              description: >-
                Seconds to wait before retrying. `30` when the cap of 10 runs is
                the cause.
        '503':
          description: >-
            The solver or the worker platform could not take the run. Nothing
            was started; submit again.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GatewayError'
              example:
                message: Failed to launch routing run
                error: GATEWAY_ERROR
components:
  schemas:
    SolveRequest:
      type: object
      description: Request body for `POST /v3/routing/solve`.
      required:
        - problem
      properties:
        options:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/SolveOptions'
              description: Solve options.
          description: Solve options.
        plan:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/Plan'
              description: >-
                Warm start: the plan the search starts from instead of
                constructing

                from scratch (re-optimization). A result's `plan` goes in as it
                stands.

                See `plan.routes[].locked_count` for freezing a dispatched
                prefix.


                A **hint, not a guarantee**: an unlocked stop that violates a
                hard

                constraint (e.g. capacity, skills, eligibility, `limits.*`, or a
                time

                window with no `per_late_hour` set) is left unassigned and the

                search reinserts it wherever it legitimately fits, or it
                surfaces in

                `unassigned[]` with a typed reason. An infeasible locked stop is
                a 400.
          description: >-
            Warm start: the plan the search starts from instead of constructing

            from scratch (re-optimization). A result's `plan` goes in as it
            stands.

            See `plan.routes[].locked_count` for freezing a dispatched prefix.


            A **hint, not a guarantee**: an unlocked stop that violates a hard

            constraint (e.g. capacity, skills, eligibility, `limits.*`, or a
            time

            window with no `per_late_hour` set) is left unassigned and the

            search reinserts it wherever it legitimately fits, or it surfaces in

            `unassigned[]` with a typed reason. An infeasible locked stop is a
            400.
        problem:
          $ref: '#/components/schemas/ProblemInput'
          description: 'The problem: tasks, fleet, depots, relations, objective, travel.'
      additionalProperties: false
      example:
        options:
          seed: 42
          time_limit_s: 5
        problem:
          depots:
            - id: gent
              location:
                - 3.7174
                - 51.0543
          jobs:
            - id: job-1
              location:
                - 3.725
                - 51.05
              pickup:
                weight: 20
              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'
            - id: job-2
              location:
                - 3.71
                - 51.06
              pickup:
                weight: 35
              service_duration_s: 600
          objective:
            costs:
              per_route_hour: 35
              per_travel_km: 0.2
            currency: EUR
          vehicles:
            - capacity:
                weight: 500
              id: v1
              profile: car
              shifts:
                - end:
                    depot: gent
                  from: '2026-07-01T08:00:00+02:00'
                  id: mon
                  start:
                    depot: gent
                  to: '2026-07-01T17:00:00+02:00'
              skills:
                - name: fridge
    Run:
      type: object
      description: One async run.
      required:
        - id
        - status
        - links
      properties:
        id:
          type: string
          format: uuid
          description: The run id. Use it in the paths of the other endpoints.
          example: d28568b1-34cc-4f89-b10b-dc24b26a8275
        status:
          $ref: '#/components/schemas/RunStatus'
        links:
          $ref: '#/components/schemas/RunLinks'
        error:
          $ref: '#/components/schemas/RunError'
          description: Present when `status` is `failed`.
        started_at:
          type: string
          format: date-time
          description: >-
            When a worker claimed the run, in UTC. Absent while the run is
            `queued`.
          example: '2026-10-15T07:40:59Z'
        finished_at:
          type: string
          format: date-time
          description: When the run reached a final state, in UTC. Absent before then.
          example: '2026-10-15T07:41:00Z'
        expires_at:
          type: string
          format: date-time
          description: >-
            When the run and its plan are deleted: `finished_at` plus 365 days.
            Absent before the run is final.
          example: '2027-10-15T07:41:00Z'
    ErrorBody:
      type: object
      description: >-
        Error body of every `/v3/routing/*` endpoint.


        `status` is the HTTP status as text, `code` says what kind of failure it

        is, `request_id` identifies the request, and `errors` holds one item per

        problem found in the request. Branch on `code`, and on `errors[].code`
        for

        400s; the messages are for people.
      required:
        - status
        - code
        - message
      properties:
        code:
          $ref: '#/components/schemas/ErrorCode'
          description: What kind of failure this is. One value per HTTP status class.
        errors:
          type: array
          items:
            $ref: '#/components/schemas/ErrorItem'
          description: >-
            One entry per problem found in the request. Empty for non-input
            errors.
        message:
          type: string
          description: >-
            Human-readable summary. With exactly one item it is that item's

            message; with several, `"{n} problems in request"` (at most 100
            items

            are listed, and the message says when the list is cut).
        request_id:
          type:
            - string
            - 'null'
          description: >-
            The `X-Request-Id` of the request (echoed, or generated when
            absent).
        status:
          type: string
          description: |-
            The HTTP status as text, e.g. `"BAD_REQUEST"` or
            `"SERVICE_UNAVAILABLE"`.
          example: BAD_REQUEST
      example:
        code: invalid_request
        errors:
          - code: out_of_range
            message: 'depot ''gent'': service_duration_s must be >= 0, got -60'
            pointer: /problem/depots/0/service_duration_s
        message: 'depot ''gent'': service_duration_s must be >= 0, got -60'
        request_id: req_7f3a2c90d1e4b5a6
        status: BAD_REQUEST
    GatewayError:
      type: object
      description: >-
        The gateway's own error answer on `/v3/routing/runs*`, not the
        `ErrorBody` shape. `message` starts with a code (`not_found`,
        `run_not_finished`, `no_result`), a colon and a sentence; branch on the
        HTTP status and that code.
      required:
        - error
      properties:
        error:
          type: string
          description: '`GATEWAY_ERROR`, or the reason for a malformed `id`.'
          example: GATEWAY_ERROR
        message:
          type: string
          description: Absent on a malformed `id`.
          example: 'not_found: routing run d28568b1-34cc-4f89-b10b-dc24b26a8275'
    SolveOptions:
      type: object
      description: Solve options.
      properties:
        polylines:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/PolylineDetail'
              description: |-
                Return road geometry for each route as `routes[].polyline`:
                `simplified` or `full`. Omitted → no geometry (the default).
          description: |-
            Return road geometry for each route as `routes[].polyline`:
            `simplified` or `full`. Omitted → no geometry (the default).
        seed:
          type:
            - integer
            - 'null'
          format: int64
          description: |-
            Random seed for the search. Default `42`: omitting it is the same as
            sending `42`, so send different values to explore different plans.
            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:
            - number
            - 'null'
          format: double
          description: >-
            Search time budget in seconds, fractions allowed (default 1).
            Maximum

            300: a larger value is rejected with HTTP 400. A value below 0.1 is

            raised to a 100 ms floor. Pure search time: fetching travel times
            and

            waiting for capacity are not deducted from it, so the request takes

            longer than this value.
          example: 2.5
          maximum: 300
          minimum: 0
        travel_fallback:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/TravelFallback'
              description: >-
                What to do when Solvice Maps cannot answer (outage, timeout,

                overload): `estimate` (the default) solves on estimated travel
                and

                sets `summary.travel_estimated`; `fail` returns the 502/503

                instead. A request Maps rejects as invalid always fails.
          description: |-
            What to do when Solvice Maps cannot answer (outage, timeout,
            overload): `estimate` (the default) solves on estimated travel and
            sets `summary.travel_estimated`; `fail` returns the 502/503
            instead. A request Maps rejects as invalid always fails.
      additionalProperties: false
      example:
        polylines: full
        seed: 42
        time_limit_s: 5
    Plan:
      type: object
      description: >-
        A plan: which stops each vehicle shift serves, in order. A result's
        `plan`

        has exactly this shape and goes back into evaluate, suggest or a

        warm-started solve unchanged.
      required:
        - routes
      properties:
        routes:
          type: array
          items:
            $ref: '#/components/schemas/PlanRoute'
          description: One entry per vehicle shift that serves something.
      additionalProperties: false
    ProblemInput:
      type: object
      description: The problem part of every V3 request body.
      required:
        - vehicles
      properties:
        depots:
          type: array
          items:
            $ref: '#/components/schemas/Depot'
          description: Depots that shifts start from, end at, or reload at, by id.
        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.
        quotas:
          type: array
          items:
            $ref: '#/components/schemas/Quota'
          description: Stop caps shared by a set of vehicles.
        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.
        travel:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/Travel'
              description: Where travel times and distances come from.
          description: Where travel times and distances come from.
        vehicles:
          type: array
          items:
            $ref: '#/components/schemas/Vehicle'
          description: Available vehicles.
      additionalProperties: false
      example:
        depots:
          - id: gent
            location:
              - 3.7174
              - 51.0543
        jobs:
          - id: job-1
            location:
              - 3.725
              - 51.05
            pickup:
              weight: 20
            service_duration_s: 300
        vehicles:
          - capacity:
              weight: 500
            id: v1
            shifts:
              - end:
                  depot: gent
                from: '2026-07-01T08:00:00+02:00'
                id: mon
                start:
                  depot: gent
                to: '2026-07-01T17:00:00+02:00'
    RunStatus:
      type: string
      enum:
        - queued
        - running
        - succeeded
        - failed
        - cancelled
      description: >-
        Where a run is. `queued` until a worker claims it, then `running`.
        `succeeded`, `failed` and `cancelled` are final.
    RunLinks:
      type: object
      description: Paths of the run, relative to the API host.
      required:
        - self
        - result
      properties:
        self:
          type: string
          description: '`GET` it to poll the run.'
          example: /v3/routing/runs/d28568b1-34cc-4f89-b10b-dc24b26a8275
        result:
          type: string
          description: '`GET` it for the plan once the run `succeeded`.'
          example: /v3/routing/runs/d28568b1-34cc-4f89-b10b-dc24b26a8275/result
    RunError:
      type: object
      description: Why a run `failed`, in the `ErrorBody` shape.
      required:
        - status
        - code
        - message
      properties:
        status:
          type: string
          description: The HTTP status as text.
          example: INTERNAL_SERVER_ERROR
        code:
          type: string
          description: >-
            `invalid_request`, `matrix_provider_error`, `internal` or
            `worker_lost`. `worker_lost`: the worker stopped before it finished,
            or the run exceeded 6,000 s.
          example: worker_lost
        message:
          type: string
          description: Human-readable summary.
          example: the worker stopped before finishing
        request_id:
          type:
            - string
            - 'null'
          description: The `X-Request-Id` of the request that submitted the run.
        errors:
          type: array
          items:
            $ref: '#/components/schemas/ErrorItem'
          description: One entry per problem found in the request, as in `ErrorBody`.
    ErrorCode:
      type: string
      description: The top-level `code` of an error body.
      enum:
        - invalid_request
        - feature_not_enabled
        - payload_too_large
        - unsupported_media_type
        - internal
        - matrix_provider_error
        - overloaded
    ErrorItem:
      type: object
      description: One problem in a request.
      required:
        - code
        - message
      properties:
        actual:
          type:
            - integer
            - 'null'
          format: int64
          description: '`limit_exceeded` only: the count this request has.'
          minimum: 0
        code:
          $ref: '#/components/schemas/ErrorItemCode'
          description: What is wrong.
        limit:
          type:
            - integer
            - 'null'
          format: int64
          description: '`limit_exceeded` only: the published cap.'
          minimum: 0
        message:
          type: string
          description: Human-readable description.
        pointer:
          type:
            - string
            - 'null'
          description: |-
            RFC 6901 JSON Pointer into the request body, when the problem has a
            location. It is relative to the root of the body, so a field of the
            problem reads `/problem/...` (e.g.
            `"/problem/vehicles/0/shifts/0/from"`).
    PolylineDetail:
      type: string
      description: >-
        How much road detail `routes[].polyline` carries.


        This is the knob that decides whether a drawn route follows the roads or

        cuts corners — **not** the polyline encoding precision, which is fixed
        at 5

        and is a sub-metre concern. Measured on a three-stop, 150 km route:


        | detail | shape points | geometry bytes | added latency |

        |--------|--------------|----------------|---------------|

        | `simplified` | 34 | ~200 | ~0 |

        | `full` | 1,728 | ~5,300 | ~0 (inside run-to-run noise) |


        Prefer `full` unless response size is the binding constraint: the extra

        detail is nearly free in time, and `simplified` is what makes a route
        look

        like straight lines between stops.
      enum:
        - simplified
        - full
      example: full
    TravelFallback:
      type: string
      description: |-
        [`SolveOptions::travel_fallback`]: what a solve does when travel times
        cannot be fetched.
      enum:
        - estimate
        - fail
      example: estimate
    PlanRoute:
      type: object
      description: >-
        One vehicle shift's route: every trip of the shift, split by `reload`
        stops.
      required:
        - vehicle
        - shift
        - stops
      properties:
        locked_count:
          type:
            - integer
            - 'null'
          format: int32
          description: >-
            On solve, the first `locked_count` stops stay where they are
            (already

            dispatched). Counts every listed stop. Cannot be combined with a

            `reload` stop, and must not exceed the route's stop count — both are

            validated on every endpoint that takes a plan. Otherwise inert on

            evaluate. On suggest, no insertion lands before them.
          example: 1
          minimum: 0
        shift:
          type: string
          description: Shift id, within that vehicle.
        stops:
          type: array
          items:
            $ref: '#/components/schemas/PlanStop'
          description: Stops in visiting order.
        vehicle:
          type: string
          description: Vehicle id.
      additionalProperties: false
      example:
        shift: mon
        stops:
          - id: boiler-17
            type: job
        vehicle: van-3
    Depot:
      type: object
      description: >-
        A depot: a place a shift starts from, ends at, or reloads at mid-shift,

        referenced by `id` from `shifts[].start`, `shifts[].end` and

        `shifts[].reloads`. Each depot keeps its own handling time and visit
        cost.
      required:
        - id
        - location
      properties:
        cost_per_visit:
          type:
            - number
            - 'null'
          format: double
          description: >-
            Money charged once per reload visit, in the objective's currency.

            Default 0. A shift that only starts or ends here is not charged.


            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.
          example: 38.66
          minimum: 0
        id:
          type: string
          description: 'Depot id: 1 to 128 characters, unique across `depots[]`.'
          example: gent
          maxLength: 128
          minLength: 1
        location:
          $ref: '#/components/schemas/Place'
          description: Where the depot is.
        service_duration_s:
          type:
            - integer
            - 'null'
          format: int32
          description: >-
            Handling time in seconds per visit: the load before a shift that
            starts

            here, the unload after a shift that ends here, and each reload. A
            shift

            end's own `service_duration_s` overrides it for that end. Default 0.

            The unload after a shift must finish by that shift's `to` (or

            `to + max_overtime_s`). Not scaled by `service_factor`: this is

            depot-side handling, not the driver's on-site work rate.
          example: 900
          minimum: 0
      additionalProperties: false
      example:
        id: gent
        location:
          - 3.7174
          - 51.0543
        service_duration_s: 900
    Visit:
      type: object
      description: |-
        A single-stop visit: one location served once (the `jobs[]` entry type).
        For a pickup bound to a delivery on the same vehicle, use `shipments[]`.
      required:
        - id
      properties:
        assignment_costs:
          type:
            - array
            - 'null'
          items:
            $ref: '#/components/schemas/AssignmentCost'
          description: >-
            Absolute price of serving this task on a given vehicle, in the

            objective's currency. Charged once, when that vehicle (on any of its

            shifts) serves the task; unlisted vehicles cost nothing. Unlike

            `preferences`, which charges the vehicles that did NOT get the task,

            this prices the vehicle that did, so a task rated badly everywhere
            is

            the first one dropped when capacity runs out. It lives in the cost
            tier,

            below `priority`, and adds to `preferences`, `drop_fee` and travel.

            Not applied by decomposition: a problem that sets it solves
            monolithically.
        break_interruptible:
          type:
            - boolean
            - 'null'
          description: |-
            Whether a break may interrupt service here. Default `true`. `false`
            is not supported yet and is rejected with HTTP 400.
          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
        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. Tasks without a `drop_fee` must be served.


            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

            `score.components.drop_fees` and summed into `score.cost`.


            On a shipment the fee applies to the pair, charged once.
          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: 'Job id: 1 to 128 characters, unique across `jobs` + `shipments`.'
          example: job-1
          maxLength: 128
          minLength: 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, load, 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).

                A shipment's legs still require a `location`.
          description: |-
            Where this visit happens. Omit for a "virtual" (location-less) job:
            it still has service duration, time windows, load, 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).
            A shipment's legs still require a `location`.
        locked_vehicle:
          type:
            - string
            - 'null'
          description: >-
            Hard-pin this job to one vehicle id — only that vehicle (any of its

            shifts) may serve it, and once placed, re-optimization never removes
            it

            or moves it to another vehicle. Stronger than `eligible_vehicles`

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


            A pin means "stays on this vehicle if placed", **not** "must be

            served": the task can still end up unassigned (for example when it
            does

            not fit), and with a `drop_fee` it then pays the fee. An id that

            matches no vehicle is rejected with HTTP 400.
          example: vehicle-1
        pickup:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/BTreeMap'
              description: >-
                Quantity picked up at this visit: goes onto the vehicle here and
                stays

                aboard until the route ends or reloads. Non-negative magnitudes
                per

                named dimension. A swap that collects one quantity and drops off

                another at the same address is one job with both `pickup` and

                `delivery`.
          description: >-
            Quantity picked up at this visit: goes onto the vehicle here and
            stays

            aboard until the route ends or reloads. Non-negative magnitudes per

            named dimension. A swap that collects one quantity and drops off

            another at the same address is one job with both `pickup` and

            `delivery`.
        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 datetime, UTC offset required, on the intended day. A
            day is the calendar date of

            the value **in its own UTC offset**, the same rule that dates a
            shift

            from its `from`, so passing the target shift's `from` verbatim is

            always correct.


            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 is part of `score.cost`,

            which ranks below serving required tasks, so a task is never left

            unassigned to honour its preferred date; `locked_vehicle` (hard)
            also

            still wins.


            Supported on shipments as well as jobs. 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 `non_preferred_date_cost` is charged
            once

            for the pair rather than once per leg.
          example: '2026-08-04T08:00:00+02:00'
        priority:
          type:
            - integer
            - 'null'
          format: int32
          description: >-
            Importance of a must-serve task when not all of them fit: the solver

            maximises the summed `priority` of served tasks before any cost. `>=
            1`,

            default `1` (maximise the count). A rank, not money — it never makes
            a

            task droppable; use `drop_fee` for that. Both set is a 400.
          example: 3
          minimum: 1
        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
        service_factors:
          type:
            - array
            - 'null'
          items:
            $ref: '#/components/schemas/ServiceFactor'
          description: >-
            Per-vehicle work-rate overrides for this task's
            `service_duration_s`,

            composed multiplicatively with the serving vehicle's or shift's

            `service_factor`. On a shipment the override scales both legs. A

            vehicle not listed works at its own rate.
        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,

            at or above each requirement's `min_level`.
        tags:
          type:
            - array
            - 'null'
          items:
            type: string
          description: >-
            Free-form task labels (zones/territories, grouping/relation labels).

            Used for `tag`/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:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/Target'
              description: |-
                Soft target for this task, measured at its arrival (default) or
                departure. On a shipment it is measured on the **delivery** leg.
          description: |-
            Soft target for this task, measured at its arrival (default) or
            departure. On a shipment it is measured on the **delivery** leg.
        time_windows:
          type:
            - array
            - 'null'
          items:
            $ref: '#/components/schemas/TimeWindow'
          description: Time windows for this visit.
      example:
        id: job-1
        location:
          - 3.725
          - 51.05
        pickup:
          weight: 20
        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.

        Plans are compared in a fixed order: first **serve the required tasks**
        (the summed `priority` of the required tasks left unassigned is
        minimised), then **minimise cost** — the `costs` rates plus every priced
        preference, reported as `score.cost`. There is no separate vehicle-count
        step: to use fewer vehicles, price them with `costs.fixed` or
        `vehicles[].costs.fixed`.
      properties:
        costs:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/Costs'
              description: >-
                Fleet cost rates: the default for every vehicle and shift that
                does

                not set its own. See the `Costs` schema.
          description: |-
            Fleet cost rates: the default for every vehicle and shift that does
            not set its own. See the `Costs` schema.
        currency:
          type:
            - string
            - 'null'
          description: >-
            Currency label (ISO 4217, e.g. `EUR`) for every amount in the
            request

            and in `score`. Informational: echoed as `score.currency`, it never

            converts anything.
          example: EUR
        grouping:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/Grouping'
              description: Job-proximity cluster cohesion. See the `Grouping` schema.
          description: Job-proximity cluster cohesion. See the `Grouping` schema.
        non_preferred_date_cost:
          type:
            - number
            - 'null'
          format: double
          description: >-
            Flat charge, in the objective's currency, for serving a task on a
            day

            other than its `preferred_date`.


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

            calendar day (in its own offset) 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.


            Part of `score.cost`, so it trades off against travel and vehicle

            cost but never against serving a required task.
          example: 50
          minimum: 0
        per_late_hour:
          type:
            - number
            - 'null'
          format: double
          description: >-
            Cost per hour of lateness: the default rate for every task window

            that does not carry its own `time_windows[].per_late_hour`. Omit for

            hard windows. An explicit `0.0` is rejected as ambiguous (free

            lateness and hard windows read opposite ways, and lateness is never

            free). There is no earliness rate. It prices task windows only: a

            shift's `to` (plus `max_overtime_s` when set) stays a hard limit.

            Setting it `> 0` alongside a nonzero `time_windows[].cost` anywhere
            in

            the request is rejected with HTTP 400.
          example: 60
          exclusiveMinimum: 0
      additionalProperties: false
      example:
        costs:
          per_route_hour: 35
          per_travel_km: 0.2
        currency: EUR
    Quota:
      type: object
      description: >-
        A hard cap on how many stops of `job_ids` the `vehicles` serve together

        in a window. A stop counts when its service starts in `[from, to)`

        (`arrival + wait_s` in the response); without a window every stop
        counts.

        Stops count as `max_tasks` counts them: a job one, a shipment two. An

        over-cap job is left unassigned. Unlike `limits.periods[]`, which counts
        a

        shift by its start, a quota has no `every` recurrence and caps only stop

        counts.
      required:
        - id
        - vehicles
        - job_ids
        - max_tasks
      properties:
        from:
          type:
            - string
            - 'null'
          format: date-time
          description: Window start (RFC 3339, UTC offset required), with `to`.
          example: '2026-10-05T00:00:00+02:00'
        id:
          type: string
          description: |-
            Quota id: 1 to 128 characters, unique among `quotas`; named in the
            `/evaluate` `quota` violation.
          maxLength: 128
          minLength: 1
        job_ids:
          type: array
          items:
            type: string
          description: Job or shipment ids whose stops count, each at most once.
        max_tasks:
          type: integer
          format: int32
          description: The cap on counted stops.
          minimum: 0
        to:
          type:
            - string
            - 'null'
          format: date-time
          description: Window end (RFC 3339, UTC offset required, exclusive), with `from`.
          example: '2026-10-06T00:00:00+02:00'
        vehicles:
          type: array
          items:
            type: string
          description: Vehicle ids that share the cap, each at most once.
      additionalProperties: false
      example:
        id: pool-install
        job_ids:
          - j1
          - j2
          - j3
        max_tasks: 2
        vehicles:
          - c0
          - c1
    Relation:
      oneOf:
        - type: object
          title: Sequence
          description: >-
            Ordering (gaps allowed). Constrains members that end up on the
            **same

            route** only: members on different vehicles are unordered relative
            to

            each other.
          required:
            - stops
            - type
          properties:
            stops:
              type: array
              items:
                type: string
              description: >-
                Job ids or shipment-leg stop ids (`pickup.id` / `delivery.id`,
                by

                default `"{shipment}:pickup"` / `"{shipment}:delivery"`), in the

                order they must be served. A shipment's own id is not resolved.
            type:
              type: string
              enum:
                - sequence
        - type: object
          title: Tag order
          description: >-
            Ordering between tags: on every route, every task carrying `tags[0]`

            precedes every task carrying `tags[1]`, and so on. Tasks carry

            membership via their own `tags`. At most one `tag_order` per
            request.


            Jobs 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 tag ordering cannot compose with yet). Drop
            the

            tag from the shipment.
          required:
            - tags
            - type
          properties:
            tags:
              type: array
              items:
                type: string
              description: Tag labels in precedence order. Requires at least 2 entries.
              minItems: 2
            type:
              type: string
              enum:
                - tag_order
        - type: object
          title: Same vehicle
          description: |-
            Same vehicle: every member that is served is served by the same
            vehicle, on any of its shifts.
          required:
            - type
          properties:
            stops:
              type:
                - array
                - 'null'
              items:
                type: string
              description: |-
                Job ids or shipment-leg stop ids in the relation (mutually
                exclusive with `tag`). A shipment's own id is not resolved.
            tag:
              type:
                - string
                - 'null'
              description: >-
                Tag selecting the relation members (mutually exclusive with
                `stops`).
            type:
              type: string
              enum:
                - same_vehicle
        - type: object
          title: Same day
          description: >-
            Same calendar day: every member that is served is served on the same

            day. The day is the calendar date of the serving shift's `from`,
            read

            in that value's own UTC offset, not the time of the stop. Like

            `same_vehicle`, it constrains only the members that end up assigned.
          required:
            - type
          properties:
            stops:
              type:
                - array
                - 'null'
              items:
                type: string
              description: |-
                Job ids or shipment-leg stop ids in the relation (mutually
                exclusive with `tag`). A shipment's own id is not resolved.
            tag:
              type:
                - string
                - 'null'
              description: >-
                Tag selecting the relation members (mutually exclusive with
                `stops`).
            type:
              type: string
              enum:
                - same_day
        - type: object
          title: Same route
          description: |-
            Same route: every member that ends up assigned is served by one
            vehicle shift (one entry of `shifts`), stricter than
            `same_vehicle` for vehicles with several `shifts`. Reloads within
            the shift do not split a route. Hard-only, with `same_vehicle`'s
            partial-service semantics.
          required:
            - type
          properties:
            stops:
              type:
                - array
                - 'null'
              items:
                type: string
              description: |-
                Job ids or shipment-leg stop ids in the relation (mutually
                exclusive with `tag`). A shipment's own id is rejected with HTTP
                400.
            tag:
              type:
                - string
                - 'null'
              description: >-
                Tag selecting the relation members (mutually exclusive with
                `stops`).
            type:
              type: string
              enum:
                - same_route
        - type: object
          title: Synchronized
          description: |-
            Synchronized starts (or ends) across vehicles: every member's
            service start — or with `on: end`, its service end — lies within
            `max_wait_s` seconds of every other member's. Hard, checked across
            routes; the returned schedule holds a member back where needed so
            the bound is met (see `hold_s` on the stop). A hold waits inside the
            regular shift only: a member is never held past its shift's `to`
            into overtime (`max_overtime_s`); a group that could only meet the
            bound that way is left unassigned instead.
          required:
            - max_wait_s
            - stops
            - type
          properties:
            max_wait_s:
              type: integer
              format: int64
              description: >-
                Maximum spread between the synchronized instants in seconds

                (`0` = exact simultaneity). Required. A negative value is
                rejected

                with HTTP 400 `out_of_range`.
              minimum: 0
            'on':
              oneOf:
                - type: 'null'
                - $ref: '#/components/schemas/StopInstant'
                  description: >-
                    Which instant is synchronized: the service `start` (default)
                    or

                    the service `end`. `arrival` is rejected with HTTP 400.
              description: |-
                Which instant is synchronized: the service `start` (default) or
                the service `end`. `arrival` is rejected with HTTP 400.
            stops:
              type: array
              items:
                type: string
              description: |-
                Participating job ids. A shipment-leg stop id is rejected with
                HTTP 400; a shipment's own id is not resolved.
            type:
              type: string
              enum:
                - synchronized
        - type: object
          title: Time lag
          description: >-
            A directional time bound between two jobs on any vehicles:

            `t(to) − t(from)` must lie in `[min_s, max_s]` seconds, each end

            measured at the job's service start or end (`on`). "B starts at

            least an hour after A ends" is `from: {stop: A, on: end}, to: {stop:
            B},

            min_s: 3600`; "A and B finish together" is both ends `on: end` with

            `min_s: 0, max_s: 0`. `min_s` defaults to `0`; an absent `max_s` is

            unbounded; both may be negative. Hard, checked across routes like

            `synchronized` (an unassigned end imposes nothing). A job may take

            part in any number of `time_lag` relations; a chain is written as

            one relation per consecutive pair. As with `synchronized`, a hold

            never waits past the shift's regular end (`to`).
          required:
            - from
            - to
            - type
          properties:
            from:
              $ref: '#/components/schemas/TimeLagEnd'
              description: The reference end.
            max_s:
              type:
                - integer
                - 'null'
              format: int64
              description: Upper bound on `t(to) − t(from)` in seconds. Absent = unbounded.
            min_s:
              type:
                - integer
                - 'null'
              format: int64
              description: Lower bound on `t(to) − t(from)` in seconds. Default `0`.
            to:
              $ref: '#/components/schemas/TimeLagEnd'
              description: The bounded end.
            type:
              type: string
              enum:
                - time_lag
        - type: object
          title: Compatible vehicles
          description: |-
            Which vehicles may serve a set of tasks together. Every two distinct
            vehicles serving `stops` must be compatible under
            `vehicles[].compatible_with`; with no lists it restricts nothing.
            Tasks left unassigned impose nothing, so a group may be served
            partially. Hard.

            Every task must also be named by a `synchronized` or `time_lag`
            relation; a task named by neither is rejected with HTTP 400. At most
            16 tasks per relation.
          required:
            - stops
            - type
          properties:
            stops:
              type: array
              items:
                type: string
              description: Job ids, at least two.
              example:
                - job-1
                - job-2
            type:
              type: string
              enum:
                - compatible_vehicles
        - type: object
          title: All or none
          description: >-
            All or none: either every task in `stops` is served, or none is.

            Sets no timing; combine with `synchronized` or `time_lag` for when.

            Hard. Overlapping `all_or_none` relations chain: one on A and B plus

            one on B and C serve all three together or none of them. A group
            with

            a task in a vehicle's locked prefix, or committed to a vehicle
            (either

            a warm-start commitment), is exempt.


            Every task must also be named by a `synchronized` or `time_lag`

            relation; a task named by neither is rejected with HTTP 400.
          required:
            - stops
            - type
          properties:
            stops:
              type: array
              items:
                type: string
              description: Job ids, at least two.
              example:
                - job-1
                - job-2
            type:
              type: string
              enum:
                - all_or_none
      description: >-
        A hard constraint between tasks, selected by `type`: `sequence`,

        `tag_order`, `same_vehicle`, `same_day`, `same_route`, `synchronized`,

        `time_lag`, `compatible_vehicles` or `all_or_none`.

        Each type carries only its own fields; any other `type` is rejected with

        HTTP 400.


        Members are named by `stops` (stop ids). `same_vehicle`, `same_day` and

        `same_route` take **either** `stops` **or** a `tag`, not both. Every

        relation is hard.


        A shipment takes part through its legs: `sequence`, `same_vehicle`,

        `same_day` and `same_route` accept a leg's stop id (`pickup.id` /

        `delivery.id`, by default `"{shipment}:pickup"` / `":delivery"`) and
        bind

        that leg like a job. A leg coupled to other tasks by a `synchronized` or

        `time_lag` relation (directly, or through a `sequence` with a member
        that

        is) is rejected with HTTP 400, as is a `sequence` putting a delivery
        before

        its pickup. A shipment's own id in the `stops` of a `sequence`,

        `same_vehicle`, `same_day` or `synchronized` relation, and a shipment

        carrying the `tag` of a `same_vehicle` or `same_day` relation, passes

        validation but is skipped: the request returns HTTP 200 and the relation

        does not constrain that shipment. `same_route`, `time_lag`,

        `compatible_vehicles`, `all_or_none` and `tag_order` reject a shipment

        with HTTP 400.
      example:
        stops:
          - job-1
          - job-2
        type: sequence
    Shipment:
      type: object
      description: >-
        A bound pickup→delivery shipment (the `shipments[]` entry type).


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

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


        `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:
        assignment_costs:
          type:
            - array
            - 'null'
          items:
            $ref: '#/components/schemas/AssignmentCost'
          description: >-
            Absolute price of serving this task on a given vehicle, in the

            objective's currency. Charged once, when that vehicle (on any of its

            shifts) serves the task; unlisted vehicles cost nothing. Unlike

            `preferences`, which charges the vehicles that did NOT get the task,

            this prices the vehicle that did, so a task rated badly everywhere
            is

            the first one dropped when capacity runs out. It lives in the cost
            tier,

            below `priority`, and adds to `preferences`, `drop_fee` and travel.

            Not applied by decomposition: a problem that sets it solves
            monolithically.
        delivery:
          $ref: '#/components/schemas/Stop'
          description: Delivery leg (load off).
        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. Tasks without a `drop_fee` must be served.


            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

            `score.components.drop_fees` and summed into `score.cost`.


            On a shipment the fee applies to the pair, charged once.
          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: >-
            Shipment id: 1 to 128 characters, unique across `jobs` +
            `shipments`.
          example: ship-1
          maxLength: 128
          minLength: 1
        load:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/BTreeMap'
              description: >-
                Load carried from the pickup to the delivery: picked up at one
                leg and

                the same quantity dropped off at the other. Non-negative
                magnitudes

                per named dimension.
          description: >-
            Load carried from the pickup to the delivery: picked up at one leg
            and

            the same quantity dropped off at the other. Non-negative magnitudes

            per named dimension.
        locked_vehicle:
          type:
            - string
            - 'null'
          description: >-
            Hard-pin this job to one vehicle id — only that vehicle (any of its

            shifts) may serve it, and once placed, re-optimization never removes
            it

            or moves it to another vehicle. Stronger than `eligible_vehicles`

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


            A pin means "stays on this vehicle if placed", **not** "must be

            served": the task can still end up unassigned (for example when it
            does

            not fit), and with a `drop_fee` it then pays the fee. An id that

            matches no vehicle is rejected with HTTP 400.
          example: vehicle-1
        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 datetime, UTC offset required, on the intended day. A
            day is the calendar date of

            the value **in its own UTC offset**, the same rule that dates a
            shift

            from its `from`, so passing the target shift's `from` verbatim is

            always correct.


            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 is part of `score.cost`,

            which ranks below serving required tasks, so a task is never left

            unassigned to honour its preferred date; `locked_vehicle` (hard)
            also

            still wins.


            Supported on shipments as well as jobs. 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 `non_preferred_date_cost` is charged
            once

            for the pair rather than once per leg.
          example: '2026-08-04T08:00:00+02:00'
        priority:
          type:
            - integer
            - 'null'
          format: int32
          description: >-
            Importance of a must-serve task when not all of them fit: the solver

            maximises the summed `priority` of served tasks before any cost. `>=
            1`,

            default `1` (maximise the count). A rank, not money — it never makes
            a

            task droppable; use `drop_fee` for that. Both set is a 400.
          example: 3
          minimum: 1
        service_factors:
          type:
            - array
            - 'null'
          items:
            $ref: '#/components/schemas/ServiceFactor'
          description: >-
            Per-vehicle work-rate overrides for this task's
            `service_duration_s`,

            composed multiplicatively with the serving vehicle's or shift's

            `service_factor`. On a shipment the override scales both legs. A

            vehicle not listed works at its own rate.
        skills:
          type:
            - array
            - 'null'
          items:
            $ref: '#/components/schemas/SkillReq'
          description: >-
            Hard skill requirements — the assigned vehicle must provide every
            one,

            at or above each requirement's `min_level`.
        tags:
          type:
            - array
            - 'null'
          items:
            type: string
          description: >-
            Free-form task labels (zones/territories, grouping/relation labels).

            Used for `tag`/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:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/Target'
              description: |-
                Soft target for this task, measured at its arrival (default) or
                departure. On a shipment it is measured on the **delivery** leg.
          description: |-
            Soft target for this task, measured at its arrival (default) or
            departure. On a shipment it is measured on the **delivery** leg.
      example:
        delivery:
          location:
            - 3.71
            - 51.06
          service_duration_s: 300
        id: ship-1
        load:
          weight: 20
        pickup:
          location:
            - 3.725
            - 51.05
          service_duration_s: 300
    Travel:
      type: object
      description: >-
        Where travel times and distances come from.


        Matrices are 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'
        engine:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/MatrixEngine'
              description: |-
                Road-network data behind travel times and distances. Omitted →
                `tomtom`.
          description: |-
            Road-network data behind travel times and distances. Omitted →
            `tomtom`.
      additionalProperties: false
      example:
        departure_time: '2026-07-01T08:00:00+02:00'
        engine: tomtom
    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.
        compatible_with:
          type:
            - array
            - 'null'
          items:
            type: string
          description: >-
            Vehicles this one may serve a `compatible_vehicles` relation's tasks

            together with, by id. Absent: any vehicle. A list is a veto, so a

            one-sided list still binds both vehicles: two vehicles may share a

            relation only if neither's list excludes the other. A vehicle always

            pairs with itself and all its shifts count as one vehicle; to pair a

            vehicle with no other, list only its own id. An empty list is
            rejected

            with HTTP 400 (it would read as "everyone" in a client that drops
            empty

            arrays), and so is an id that names no vehicle. Inert unless a

            `compatible_vehicles` relation names tasks.
          example:
            - v2
            - v3
        costs:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/Costs'
              description: >-
                This vehicle's costs. Each field set here replaces the same
                field of

                `objective.costs` for this vehicle; fields not set keep the
                fleet

                value. A shift's own `costs` replace these per field in turn.
          description: |-
            This vehicle's costs. Each field set here replaces the same field of
            `objective.costs` for this vehicle; fields not set keep the fleet
            value. A shift's own `costs` replace these per field in turn.
        id:
          type: string
          description: >-
            Vehicle id: 1 to 128 characters, unique across `vehicles[]`; echoed
            as

            `routes[].vehicle`.
          example: v1
          maxLength: 128
          minLength: 1
        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. 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:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/Profile'
              description: >-
                Routing profile: `car` (default), `truck` or `bike` — selects
                the road

                network and speeds behind this vehicle's travel times and
                distances.

                Any other value is rejected with HTTP 400.
          description: >-
            Routing profile: `car` (default), `truck` or `bike` — selects the
            road

            network and speeds behind this vehicle's travel times and distances.

            Any other value is rejected with HTTP 400.
        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),

            depot handling (`depots[].service_duration_s`,

            `start`/`end.service_duration_s`), waiting time, or break durations.


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

            day with `shifts[].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:
            $ref: '#/components/schemas/VehicleSkill'
          description: >-
            Capabilities the vehicle provides. A job's `skills` must all appear

            here at a level of at least the job's `min_level`. Each entry is an

            object `{ "name": ..., "level": N }` (`level` optional, default 1);

            a bare string is rejected. A skill may appear once per list.
            Omitting

            the field is the same as `[]`: the vehicle can only serve jobs that

            require no skills.
        speed_factor:
          type:
            - number
            - 'null'
          format: double
          description: >-
            Travel-speed multiplier (1.0 = base speed). Effective travel time =

            `base travel 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 `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:
              location:
                - 3.7174
                - 51.0543
            from: '2026-07-01T08:00:00+02:00'
            id: mon
            start:
              location:
                - 3.7174
                - 51.0543
            to: '2026-07-01T17:00:00+02:00'
        skills:
          - name: fridge
    ErrorItemCode:
      type: string
      description: |-
        The `code` of one `errors[]` item. Clients should accept values they do
        not know yet.
      enum:
        - invalid_json
        - invalid_type
        - unknown_field
        - missing_field
        - invalid_value
        - not_supported
        - out_of_range
        - duplicate_id
        - unknown_reference
        - conflicting_fields
        - limit_exceeded
    PlanStop:
      oneOf:
        - type: object
          title: Job
          description: A job, by its id.
          required:
            - id
            - type
          properties:
            id:
              type: string
              description: Job id.
              example: boiler-17
            type:
              type: string
              enum:
                - job
        - type: object
          title: Pickup
          description: A shipment's pickup leg.
          required:
            - id
            - type
          properties:
            id:
              type: string
              description: 'The leg''s stop id: its own `id`, else `{shipment}:pickup`.'
              example: parts-9:pickup
            type:
              type: string
              enum:
                - pickup
        - type: object
          title: Delivery
          description: A shipment's delivery leg.
          required:
            - id
            - type
          properties:
            id:
              type: string
              description: 'The leg''s stop id: its own `id`, else `{shipment}:delivery`.'
              example: parts-9:delivery
            type:
              type: string
              enum:
                - delivery
        - type: object
          title: Reload
          description: 'A reload at a depot: the next trip starts there.'
          required:
            - depot
            - type
          properties:
            depot:
              type: string
              description: A depot id from the shift's `reloads`.
              example: gent
            type:
              type: string
              enum:
                - reload
      description: |-
        One stop of a plan route. Start, end and breaks are placed by the solver
        and are not listed.
      example:
        id: parts-9:pickup
        type: pickup
    Place:
      type: array
      items:
        type: number
        format: double
      description: >-
        A place: a coordinate `[longitude, latitude]` in GeoJSON order — exactly
        two numbers. Tasks at the same coordinate are treated as one location.
      examples:
        - - 3.725
          - 51.05
      maxItems: 2
      minItems: 2
    AssignmentCost:
      type: object
      description: Absolute price of serving one task on one vehicle.
      required:
        - vehicle
        - cost
      properties:
        cost:
          type: number
          format: double
          description: |-
            Cost, in the objective's currency, incurred when this vehicle serves
            the task. Must be finite and `>= 0`. A vehicle not listed costs `0`.
          example: 12.5
        vehicle:
          type: string
          description: >-
            Vehicle id; applies to every shift of that vehicle. An id that
            matches

            no vehicle is rejected with HTTP 400.
          example: v1
      additionalProperties: false
      example:
        cost: 12.5
        vehicle: v1
    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
    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
    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, in the objective's currency, incurred when a different vehicle
            serves the job. Must be finite and `>= 0`.
          example: 50
      additionalProperties: false
      example:
        vehicle: v1
        violation_cost: 50
    ServiceFactor:
      type: object
      description: Per-vehicle work-rate override for one task's `service_duration_s`.
      required:
        - vehicle
        - factor
      properties:
        factor:
          type: number
          format: double
          description: >-
            Same meaning, direction and `(0, 5]` range as
            `vehicles[].service_factor`:

            effective service time = `service_duration_s /
            (vehicle_or_shift_factor * factor)`.

            `1.0` is a no-op.
          example: 1.25
          maximum: 5
          exclusiveMinimum: 0
        vehicle:
          type: string
          description: Vehicle id; applies to every shift of that vehicle.
          example: v1
      additionalProperties: false
      example:
        factor: 1.25
        vehicle: v1
    SkillReq:
      type: object
      description: A hard skill requirement — the assigned vehicle must provide it.
      required:
        - name
      properties:
        min_level:
          type:
            - integer
            - 'null'
          format: int32
          description: |-
            Minimum level the serving vehicle must hold for this skill (`>= 1`).
            Absent = 1. A vehicle skill with no `level` holds level 1.
          example: 2
          minimum: 1
        name:
          type: string
          description: Skill name the assigned vehicle must provide.
          example: fridge
      additionalProperties: false
      example:
        name: fridge
    Target:
      type: object
      description: |-
        Soft target: reaching the task's `on` instant 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 is early.
      required:
        - at
        - per_late_hour
      properties:
        at:
          type: string
          format: date-time
          description: >-
            Target instant (RFC 3339, UTC offset required) the `on` instant is
            measured against.
          example: '2026-07-01T10:00:00+02:00'
        'on':
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/StopInstant'
              description: >-
                Which instant of the task is compared to `at`: `arrival`
                (default) or

                `end`. `start` is rejected with HTTP 400.
          description: >-
            Which instant of the task is compared to `at`: `arrival` (default)
            or

            `end`. `start` is rejected with HTTP 400.
        per_late_hour:
          type: number
          format: double
          description: |-
            Cost, in the objective's currency, per hour of `on` beyond `at`
            (linear, prorated per second). Must be `> 0`; independent of
            `objective.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'
        'on': arrival
        per_late_hour: 60
    TimeWindow:
      type: object
      description: |-
        A time window. Hard unless it (or `objective.per_late_hour`, for
        every window) prices lateness.
      required:
        - from
        - to
      properties:
        cost:
          type:
            - number
            - 'null'
          format: double
          description: >-
            Optional flat cost, in the objective's currency, added when this

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

            alternative window on the same task. Absent = 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. 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 has its overlapping windows merged).


            **Cannot be combined with `objective.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 (RFC 3339 instant, UTC offset required).
          example: '2026-07-01T09:00:00+02:00'
        per_late_hour:
          type:
            - number
            - 'null'
          format: double
          description: >-
            Makes this window soft on its late side: arriving after `to` is

            allowed and priced at this rate, in the objective's currency per
            hour

            late, in place of `objective.per_late_hour` (and whether or not

            that is set, so hard and soft windows can share one request). Must
            be

            `> 0`; omit it for a hard window. Only on a

            task with exactly one window. Arriving early still waits for `from`.
          example: 60
          exclusiveMinimum: 0
        to:
          type: string
          format: date-time
          description: Latest allowed arrival (RFC 3339 instant, UTC offset required).
          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: >-
        What running a vehicle costs, in the request's currency

        (`objective.currency` is only a label). The same type at three levels:

        `objective.costs` prices the fleet, `vehicles[].costs` replaces a field
        for

        one vehicle, and `shifts[].costs` replaces it for one shift. Each field

        resolves on its own: the shift's value, else the vehicle's, else the

        fleet's, else the default. Nothing is added on top of a fleet rate, and

        every field is reported under the same `score.components` key at every

        level. `0` means priced at zero; absent means inherit.


        Rates are rounded to the internal grid (1/100 000 of a currency unit per

        second or per metre); a nonzero rate too small to represent is rejected.

        Every value must be finite and `>= 0`.
      properties:
        fixed:
          type:
            - number
            - 'null'
          format: double
          description: >-
            Flat charge for each used shift (a route with at least one task),

            reported in `score.components.fixed`. Default `0`. Pricing vehicles
            is

            how the solver is made to use fewer of them: the higher it is, the

            more travel it accepts to avoid opening another route.
          example: 50
          maximum: 1000000000
          minimum: 0
        per_overtime_hour:
          type:
            - number
            - 'null'
          format: double
          description: >-
            Per hour a route finishes past its shift's `to` (possible only with

            `max_overtime_s`), reported in `score.components.overtime`, on top
            of

            `per_route_hour`. Default `0`: overtime is unpriced.
          example: 0
          minimum: 0
        per_route_hour:
          type:
            - number
            - 'null'
          format: double
          description: |-
            Per hour of each route's wall-clock span (travel, service, waiting
            and breaks), reported in `score.components.time`. Default **35**.
          example: 35
          minimum: 0
        per_travel_km:
          type:
            - number
            - 'null'
          format: double
          description: |-
            Per kilometre driven, reported in `score.components.distance`.
            Default **0.20**.
          example: 0.2
          minimum: 0
        per_wait_hour:
          type:
            - number
            - 'null'
          format: double
          description: |-
            Per hour spent waiting for a time window to open, reported in
            `score.components.waiting`, on top of `per_route_hour` (the span
            includes the wait). Default `0`.
          example: 0
          minimum: 0
      additionalProperties: false
      example:
        fixed: 50
        per_route_hour: 35
        per_travel_km: 0.2
    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, in the objective's

            currency. At most 1000 currency units: a larger value is rejected
            with

            HTTP 400. Higher keeps clusters together more aggressively at the

            expense of total travel.
          example: 0.5
          maximum: 1000
          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
    StopInstant:
      type: string
      description: >-
        Which instant of a stop a `target` or a relation refers to. One
        vocabulary

        for both, each accepting the values that mean something there.
      enum:
        - arrival
        - start
        - end
    TimeLagEnd:
      type: object
      description: One end of a `time_lag` relation.
      required:
        - stop
      properties:
        'on':
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/StopInstant'
              description: >-
                Which instant of the job the bound is measured at: `start`
                (default) or

                `end`. `arrival` is rejected with HTTP 400.
          description: >-
            Which instant of the job the bound is measured at: `start` (default)
            or

            `end`. `arrival` is rejected with HTTP 400.
        stop:
          type: string
          description: A job id. A shipment or a shipment leg is rejected with HTTP 400.
          example: job-1
      additionalProperties: false
      example:
        'on': end
        stop: job-1
    Stop:
      type: object
      description: One leg of a shipment (pickup or delivery).
      required:
        - location
      properties:
        break_interruptible:
          type:
            - boolean
            - 'null'
          description: |-
            Whether a break may interrupt service here. Default `true`. `false`
            is not supported yet and is rejected with HTTP 400.
          example: true
        id:
          type:
            - string
            - 'null'
          description: >-
            Stop id: 1 to 128 characters when set. 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
          maxLength: 128
          minLength: 1
        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:
          - 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'
    MatrixEngine:
      type: string
      description: |-
        Which road-network data prices travel times and distances.

        - `tomtom` (default): TomTom historical-typical traffic at the departure
          time.
        - `osm`: OpenStreetMap free-flow times. No traffic, so
          `travel.departure_time` is rejected with 400.
        - `tomtom_real_time`: TomTom live traffic at the moment of the request.
          **Not yet enabled: currently always rejected with 403 `feature_not_enabled`.** Once enabled:
          `travel.departure_time` is rejected; every vehicle must use the `car`
          profile; at most 100 distinct locations; up to ~25 s of matrix fetch at
          100 locations.
      enum:
        - tomtom
        - osm
        - tomtom_real_time
      example: osm
    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_leg_travel_time_s:
          type:
            - integer
            - 'null'
          format: int64
          description: >-
            Hard cap in seconds on any single travel leg of this vehicle's
            routes:

            the `travel_s` reported on each stop, start-to-first-task and

            last-task-to-`end` included. Travel only: setup, service and wait do

            not count, and `speed_factor` applies first. A leg made free by

            `ignore_start_leg`/`ignore_end_leg`, or absent on an open start or

            end, is exempt. Absent = no cap. Must be > 0: `0` would make every

            leg too long rather than mean "no cap", so it is rejected with HTTP

            400 `out_of_range`, as is a value that does not fit in 32 bits.
          example: 3600
          minimum: 1
        max_route_duration_s:
          type:
            - integer
            - 'null'
          format: int64
          description: |-
            Hard maximum route duration in seconds: the whole-route span —
            travel, service, and wait all count, depot load and unload included.
            The same quantity as the response's `routes[].time.span_s`.
          example: 32400
        max_tasks:
          type:
            - integer
            - 'null'
          format: int32
          description: |-
            Maximum stops served per shift: a job counts as one stop, a shipment
            as two (pickup and delivery).
          example: 50
          minimum: 0
        periods:
          type: array
          items:
            $ref: '#/components/schemas/PeriodLimit'
          description: >-
            Hard caps on this vehicle's totals across all of its shifts in a
            day,

            a week or an explicit window. Each shift counts toward the one

            period that contains its `from`, read in that shift's own UTC
            offset.

            A recurring period with none of the vehicle's shifts is skipped; an

            explicit window with no shift start is a 400.
      additionalProperties: false
      example:
        max_distance_m: 200000
        max_route_duration_s: 32400
    TagPreference:
      type: object
      description: A soft tag preference, priced.
      required:
        - tag
        - violation_cost
      properties:
        tag:
          type: string
          description: Preferred tag.
          example: north
        violation_cost:
          type: number
          format: double
          description: |-
            Cost, in the objective's currency, incurred when a task without this
            tag is served.
          example: 25
      additionalProperties: false
      example:
        tag: north
        violation_cost: 25
    Profile:
      type: string
      description: A vehicle's routing profile.
      enum:
        - car
        - truck
        - bike
      example: car
    Shift:
      type: object
      description: One working shift for a vehicle.
      required:
        - id
        - from
        - to
      properties:
        breaks:
          type:
            - array
            - 'null'
          items:
            $ref: '#/components/schemas/Break'
          description: >-
            Scheduled driver breaks within this shift — see the `Break` schema
            for

            the supported forms. Injected breaks appear as `break` stops in the

            response routes.
        capacity:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/BTreeMap'
              description: >-
                Capacity for this shift only. Each dimension named here replaces
                the

                vehicle's value for that dimension; dimensions not named keep
                the

                vehicle's `capacity`.
          description: |-
            Capacity for this shift only. Each dimension named here replaces the
            vehicle's value for that dimension; dimensions not named keep the
            vehicle's `capacity`.
        costs:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/Costs'
              description: >-
                Costs for this shift only. Each field set here replaces the same

                field of the vehicle's `costs` (or, when the vehicle does not
                set it,

                of `objective.costs`) for this shift; fields not set keep that
                value.
          description: >-
            Costs for this shift only. Each field set here replaces the same

            field of the vehicle's `costs` (or, when the vehicle does not set
            it,

            of `objective.costs`) for this shift; fields not set keep that
            value.
        end:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/ShiftEnd'
              description: Where the shift ends. Absent = open end.
          description: Where the shift ends. Absent = open end.
        from:
          type: string
          format: date-time
          description: Shift start (RFC 3339 instant, UTC offset required).
          example: '2026-07-01T08:00:00+02:00'
        id:
          type: string
          description: 'Shift id: 1 to 128 characters, unique within this vehicle.'
          example: mon
          maxLength: 128
          minLength: 1
        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.
          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.
        limits:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/ShiftLimits'
              description: |-
                Limits for this shift only. Each field set here replaces the
                vehicle's `limits` value; fields not set keep it.
          description: |-
            Limits for this shift only. Each field set here replaces the
            vehicle's `limits` value; fields not set keep it.
        max_overtime_s:
          type:
            - integer
            - 'null'
          format: int32
          description: |-
            Maximum allowed overtime beyond `to` in seconds. Overtime is reached
            by working late, never by waiting: a `synchronized` or `time_lag`
            hold stops at `to`.
          example: 1800
        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`;

            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:
            type: string
          description: >-
            Depots (by `depots[].id`, each at most once) the vehicle may return
            to

            mid-shift to reload and start a fresh trip. Each visit takes that

            depot's `service_duration_s` and `cost_per_visit`. Cap trips with

            `max_trips`.
        serves_tags:
          type:
            - array
            - 'null'
          items:
            type: string
          description: >-
            Hard tag scope: 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

            `vehicles[].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 handling

            (`depots[].service_duration_s`, `start`/`end.service_duration_s`),

            waiting, or breaks.
          maximum: 5
          exclusiveMinimum: 0
        skills:
          type:
            - array
            - 'null'
          items:
            $ref: '#/components/schemas/VehicleSkill'
          description: >-
            Capabilities this shift provides, replacing the vehicle's `skills`

            for this shift only (a technician covering another region on one

            day, a crew that only carries a certification on some shifts). A

            job's `skills` must all appear in the skills of the shift that

            serves it, at a level of at least the job's `min_level`. Same entry

            shape as `vehicles[].skills`. Absent = the vehicle's `skills`; `[]`
            =

            this shift provides none.
        start:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/ShiftEnd'
              description: Where the shift starts. Absent = open start.
          description: Where the shift starts. Absent = open start.
        to:
          type: string
          format: date-time
          description: >-
            Shift end (RFC 3339 instant, UTC offset required; hard unless
            `max_overtime_s` set). The

            route must be finished by then, end-depot unload included.
          example: '2026-07-01T17:00:00+02:00'
      additionalProperties: false
      example:
        end:
          location:
            - 3.7174
            - 51.0543
        from: '2026-07-01T08:00:00+02:00'
        id: mon
        start:
          location:
            - 3.7174
            - 51.0543
        to: '2026-07-01T17:00:00+02:00'
    VehicleSkill:
      type: object
      description: |-
        A capability a vehicle or shift provides, at a proficiency level:
        `{ "name": "gas", "level": 3 }`. Always an object; a bare string is
        rejected at parse.
      required:
        - name
      properties:
        level:
          type:
            - integer
            - 'null'
          format: int32
          description: |-
            Proficiency level, `>= 1`; absent = 1. Satisfies every job
            requirement of this skill whose `min_level` is at most this value.
          example: 3
          minimum: 1
        name:
          type: string
          description: Skill name.
          example: gas
      additionalProperties: false
      example:
        level: 3
        name: gas
    GroupingDistance:
      type: string
      description: How neighbour distance is measured for `objective.grouping`.
      enum:
        - straight_line
        - road
      example: straight_line
    PeriodLimit:
      type: object
      description: >-
        A cap on a vehicle's totals across every shift that starts in one
        period.

        Set exactly one of `every`, or `from` and `to`; and at least one
        maximum.
      properties:
        every:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/PeriodEvery'
              description: >-
                A recurring period: every `day`, or every `week` (weeks start
                Monday).
          description: >-
            A recurring period: every `day`, or every `week` (weeks start
            Monday).
        from:
          type:
            - string
            - 'null'
          format: date-time
          description: >-
            Start of one explicit period (RFC 3339, UTC offset required), with
            `to`. The window must

            hold at least one of the vehicle's shift starts; a period inside one

            shift is not supported.
          example: '2026-10-05T00:00:00+02:00'
        max_distance_m:
          type:
            - integer
            - 'null'
          format: int64
          description: Hard cap on the summed distance driven, in metres.
          example: 900000
        max_duration_s:
          type:
            - integer
            - 'null'
          format: int64
          description: >-
            Hard cap on the summed route span of the period's shifts, measured
            as

            for `max_route_duration_s`. An unused shift adds nothing.
          example: 144000
        max_tasks:
          type:
            - integer
            - 'null'
          format: int32
          description: |-
            Hard cap on served stops, counted as a shift's `max_tasks` counts
            them: a job is one stop, a shipment two (pickup and delivery).
          example: 40
          minimum: 0
        to:
          type:
            - string
            - 'null'
          format: date-time
          description: >-
            End of one explicit period (RFC 3339, UTC offset required,
            exclusive), with `from`.
          example: '2026-10-12T00:00:00+02:00'
      additionalProperties: false
      example:
        every: week
        max_duration_s: 144000
        max_tasks: 40
    Break:
      oneOf:
        - type: object
          title: Windowed
          description: A break that must start inside a time window.
          required:
            - duration_s
            - windows
            - type
          properties:
            duration_s:
              type: integer
              format: int32
              description: Break duration in seconds.
            location:
              oneOf:
                - type: 'null'
                - $ref: '#/components/schemas/Place'
                  description: 'Where the break is taken. Absent: wherever the vehicle is.'
              description: 'Where the break is taken. Absent: wherever the vehicle is.'
            type:
              type: string
              enum:
                - windowed
            windows:
              type: array
              items:
                $ref: '#/components/schemas/BreakWindow'
              description: |-
                The window bounding the break's **start**. Exactly one entry; a
                second one is rejected with HTTP 400 (`not_supported`).
              minItems: 1
        - type: object
          title: Triggered
          description: A break taken once drive or duty time passes a threshold.
          required:
            - duration_s
            - after
            - threshold_s
            - type
          properties:
            after:
              $ref: '#/components/schemas/TriggerAfter'
              description: 'What to accumulate: drive time or duty time.'
            duration_s:
              type: integer
              format: int32
              description: Break duration in seconds.
            location:
              oneOf:
                - type: 'null'
                - $ref: '#/components/schemas/Place'
                  description: 'Not supported yet: any value is rejected with HTTP 400.'
              description: 'Not supported yet: any value is rejected with HTTP 400.'
            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 not supported yet and is rejected with HTTP
                400.
              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
            type:
              type: string
              enum:
                - triggered
        - 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 (RFC 3339 instant, UTC offset required).
            to:
              type: string
              format: date-time
              description: End of the break (RFC 3339 instant, UTC offset required).
            type:
              type: string
              enum:
                - fixed
      description: >-
        A scheduled break within a shift. Tagged by `type`:


        - `windowed`: a break of `duration_s` that starts inside its window.

        - `triggered`: a break of `duration_s` taken once drive or duty time
          passes `threshold_s` (the EU-561 mechanism).
        - `fixed`: a hard off-duty interval from `from` to `to`.


        Breaks are injected between stops and appear in the response as `break`

        stops.


        **Not yet supported** — rejected with HTTP 400 `not_supported`: more
        than

        one `windows` entry, a `location` on a `triggered` break, and

        `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 400, `unknown_field`), like any
        other extra key.
      example:
        after: drive
        duration_s: 1800
        threshold_s: 14400
        type: triggered
    ShiftEnd:
      type: object
      description: 'Where a shift starts or ends: exactly one of `location` or `depot`.'
      properties:
        depot:
          type:
            - string
            - 'null'
          description: A `depots[].id`. Excludes `location`.
          example: gent
        location:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/Place'
              description: An inline place. Excludes `depot`.
          description: An inline place. Excludes `depot`.
        service_duration_s:
          type:
            - integer
            - 'null'
          format: int32
          description: >-
            Handling time in seconds at this end: the morning load at `start`,
            the

            evening unload at `end`. Overrides the depot's `service_duration_s`;

            default: the depot's value, or 0 for a `location` end. Not applied
            on

            an ignored leg (`ignore_start_leg`/`ignore_end_leg`). Not scaled by

            `service_factor`.


            The unload must finish by the shift's `to` (or `to +
            max_overtime_s`):

            a route whose unload would run past it is infeasible. A shift whose

            load and unload alone exceed it is rejected with HTTP 400.
          example: 600
          minimum: 0
      additionalProperties: false
      example:
        depot: gent
    ShiftLimits:
      type: object
      description: |-
        Per-shift limits: the route-level `limits` fields a shift may override.
        Period limits apply across shifts, so they stay on the vehicle.
      properties:
        max_distance_m:
          type:
            - integer
            - 'null'
          format: int64
          description: Hard cap on this shift's route distance, in metres.
          example: 200000
        max_route_duration_s:
          type:
            - integer
            - 'null'
          format: int64
          description: Hard cap on this shift's route span, in seconds.
          example: 32400
        max_tasks:
          type:
            - integer
            - 'null'
          format: int32
          description: |-
            Maximum stops served in this shift: a job counts as one stop, a
            shipment as two (pickup and delivery).
          example: 50
          minimum: 0
      additionalProperties: false
      example:
        max_tasks: 12
    PeriodEvery:
      type: string
      description: The recurrence of a period limit (`limits.periods[].every`).
      enum:
        - day
        - week
    BreakWindow:
      type: object
      description: The window a `windowed` break must start in. Always hard.
      required:
        - from
        - to
      properties:
        from:
          type: string
          format: date-time
          description: Earliest break start (RFC 3339 instant, UTC offset required).
        to:
          type: string
          format: date-time
          description: Latest break start (RFC 3339 instant, UTC offset required).
      additionalProperties: false
      example:
        from: '2026-07-01T12:00:00+02:00'
        to: '2026-07-01T13:30:00+02:00'
    TriggerAfter:
      type: string
      description: What a break trigger accumulates.
      enum:
        - drive
        - duty
      example: drive
  securitySchemes:
    apiKey:
      type: apiKey
      in: header
      name: Authorization

````

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