# The Lanoo API, as we intend to build it (decision 0023: docs first). The
# docs pages are rendered from this file and nothing else. Once the Rust code
# exists it generates this file, and the docs read that one instead.
#
# Two extensions the docs use:
#   x-object on a tag     the schema its reference page opens with
#   x-errors on an op     the error codes that call can return
openapi: 3.1.0
info:
  title: Lanoo API
  version: "2026-10-30"
  summary: Reserve a spot for a robotaxi between rides.
  description: |
    One API to find a free spot in a shared staging area, hold it for one
    vehicle, and check in and out. Every staging area on Lanoo, for every
    operator, through the same connection.

    Versions are named by the date they were released. Send the one you
    built against in a `Lanoo-Version` header; without it, a call uses the
    version your account was pinned to on its first call. A version never
    changes in a way that breaks you; new fields may appear in it, so
    ignore fields you do not know.

    The API is in development. Nothing here can be called yet, and any of it
    may change before it can.
servers:
  - url: https://api.golanoo.com/v1
    description: Live and test, told apart by the key you use

security:
  - apiKey: []

tags:
  - name: Staging areas
    x-object: StagingArea
    description: |
      A place a city or a property has set aside for robotaxis to wait in,
      with a number of spots and the rules its owner sets. You read them;
      their owners draw and run them in the Lanoo console.
  - name: Availability
    x-object: Availability
    description: |
      Where there is room, now or at the time your vehicle would arrive.
      Availability gives counts only: you never see another operator's
      vehicles or bookings.
  - name: Reservations
    x-object: Reservation
    description: |
      A spot held for one vehicle in one staging area. It is held until the
      vehicle arrives or the hold runs out; then the vehicle checks in, and
      later checks out.

      A reservation moves through these statuses, and never back:
      `held` → `checked_in` → `checked_out`. A held reservation can instead
      end as `expired` (the vehicle did not arrive in time) or `cancelled`
      (by you, or by the staging area closing).
  - name: Webhooks
    x-object: Event
    description: |
      We tell your server when something changes that you did not cause: a
      hold running out, a staging area closing, a rule changing. Nothing is
      sent for what you did yourself, such as checking in or cancelling.
      Register an endpoint, and we send each event to it as a signed POST.

      Each POST carries a `Lanoo-Signature` header,
      `t=<unix seconds>,v1=<hex>`, where `v1` is an HMAC-SHA256 of
      `<t>.<raw body>` with the endpoint's secret. Compare it in constant
      time and reject a `t`, the time we sent it, more than 5 minutes from
      your clock either way. Answer with a 2xx;
      anything else, or no answer, counts as failed, and failed deliveries
      are retried with backoff. Events can arrive more than once and out of
      order.

paths:
  /staging_areas:
    get:
      tags: [Staging areas]
      operationId: listStagingAreas
      summary: List staging areas
      description: |
        Every staging area your fleet may use, nearest first when you pass
        `near`.
      parameters:
        - $ref: "#/components/parameters/Near"
        - $ref: "#/components/parameters/RadiusM"
        - name: city
          in: query
          description: Only staging areas in this city, by name.
          schema: { type: string, example: Copenhagen }
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/StartingAfter"
      responses:
        "200":
          description: A page of staging areas.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StagingAreaList"
              example:
                object: list
                has_more: false
                data:
                  - id: sa_norreport
                    object: staging_area
                    name: Nørreport
                    city: Copenhagen
                    country: DK
                    location: { lat: 55.6838, lng: 12.5721 }
                    spots: 6
                    status: open
                    closures:
                      - starts_at: "2026-11-09T06:00:00Z"
                        ends_at: "2026-11-09T14:00:00Z"
                        spots: ["5", "6"]
                        reason: Resurfacing
                    rules:
                      hold_minutes: 5
                      book_ahead_minutes: 120
                      max_stay_minutes: 45
                      max_vehicle_length_m: 5.2
                    updated_at: "2026-10-28T09:12:00Z"
  /staging_areas/{id}:
    get:
      tags: [Staging areas]
      operationId: getStagingArea
      summary: Get a staging area
      parameters:
        - $ref: "#/components/parameters/StagingAreaId"
      responses:
        "200":
          description: The staging area.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StagingArea"
      x-errors:
        - { status: 404, code: not_found, description: No staging area with this id is open to your fleet. }

  /availability:
    get:
      tags: [Availability]
      operationId: getAvailability
      summary: Find where there is room
      description: |
        Staging areas with room, now or at `arrive_at`, nearest first. Pass
        `near` to search around a point, or `staging_area` to ask about one.

        For a time ahead, `chance_free` is how likely it is that a spot is
        still free when the vehicle gets there. It is a forecast, not a hold:
        to be sure of a spot, reserve it.
      parameters:
        - $ref: "#/components/parameters/Near"
        - $ref: "#/components/parameters/RadiusM"
        - name: staging_area
          in: query
          description: Ask about this one staging area instead of searching.
          schema: { type: string, example: sa_norreport }
        - name: arrive_at
          in: query
          description: When the vehicle would arrive. Defaults to now.
          schema: { type: string, format: date-time, example: "2026-11-02T14:02:00Z" }
        - $ref: "#/components/parameters/Limit"
        - name: starting_after
          in: query
          description: For the next page, the `staging_area` of the last item on this one.
          schema: { type: string }
      responses:
        "200":
          description: Staging areas with room.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AvailabilityList"
              example:
                object: list
                has_more: false
                data:
                  - object: availability
                    staging_area: sa_norreport
                    name: Nørreport
                    distance_m: 420
                    free_spots: 3
                    chance_free: 0.92
                  - object: availability
                    staging_area: sa_israels_plads
                    name: Israels Plads
                    distance_m: 780
                    free_spots: 1
                    chance_free: 0.61
      x-errors:
        - { status: 400, code: parameter_missing, description: Neither `near` nor `staging_area` was given. }

  /reservations:
    post:
      tags: [Reservations]
      operationId: createReservation
      summary: Reserve a spot
      description: |
        Holds a spot in the staging area you name, for one vehicle, until
        `held_until`. The vehicle should check in before then, or the hold
        runs out and the spot goes to whoever is next.

        If the staging area is full or closed, nothing is held and the error
        offers others nearby with room. A planned closure counts: a spot in
        one of its `closures` is not held for a time inside it. Send an
        `Idempotency-Key`, so a request sent twice reserves once.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ReservationCreate"
            example:
              vehicle_id: rt-0417
              staging_area: sa_norreport
              arrive_by: "2026-11-02T14:02:00Z"
      responses:
        "201":
          description: The reservation, held.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Reservation"
              example:
                id: res_8f2c41
                object: reservation
                vehicle_id: rt-0417
                staging_area: sa_norreport
                spot: "2"
                location: { lat: 55.6838, lng: 12.5721 }
                status: held
                arrive_by: "2026-11-02T14:02:00Z"
                held_until: "2026-11-02T14:07:00Z"
                checked_in_at: null
                leave_by: null
                checked_out_at: null
                cancellation_reason: null
                metadata: {}
                created_at: "2026-11-02T13:51:12Z"
      x-errors:
        - { status: 409, code: no_spot_available, description: "The staging area is full at that time. `alternatives` lists others nearby with room." }
        - { status: 409, code: staging_area_closed, description: "The staging area is closed at that time. `alternatives` lists others nearby with room." }
        - { status: 404, code: not_found, param: staging_area, description: No staging area with this id is open to your fleet. }
        - { status: 409, code: vehicle_already_reserved, description: The vehicle already has a reservation that is held or checked in. }
        - { status: 422, code: arrive_by_out_of_range, description: "`arrive_by` is in the past, or more than the staging area's `book_ahead_minutes` ahead." }
        - { status: 422, code: vehicle_too_large, description: "`vehicle_length_m` is longer than the staging area's `max_vehicle_length_m`." }
    get:
      tags: [Reservations]
      operationId: listReservations
      summary: List reservations
      description: Your fleet's reservations, newest first. You only ever see your own.
      parameters:
        - name: status
          in: query
          description: Only reservations with this status.
          schema: { $ref: "#/components/schemas/ReservationStatus" }
        - name: vehicle_id
          in: query
          description: Only this vehicle's reservations.
          schema: { type: string, example: rt-0417 }
        - name: staging_area
          in: query
          description: Only reservations in this staging area.
          schema: { type: string, example: sa_norreport }
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/StartingAfter"
      responses:
        "200":
          description: A page of reservations.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ReservationList"
  /reservations/{id}:
    get:
      tags: [Reservations]
      operationId: getReservation
      summary: Get a reservation
      parameters:
        - $ref: "#/components/parameters/ReservationId"
      responses:
        "200":
          description: The reservation.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Reservation"
      x-errors:
        - { status: 404, code: not_found, description: No reservation with this id belongs to your fleet. }
    patch:
      tags: [Reservations]
      operationId: updateReservation
      summary: Change the arrival time
      description: |
        For a vehicle running late or early. Only a held reservation can be
        changed. The spot stays the same if it is free at the new time;
        otherwise nothing changes and the error says so.
      parameters:
        - $ref: "#/components/parameters/ReservationId"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ReservationUpdate"
            example:
              arrive_by: "2026-11-02T14:10:00Z"
      responses:
        "200":
          description: The reservation, with its new `arrive_by` and `held_until`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Reservation"
      x-errors:
        - { status: 404, code: not_found, description: No reservation with this id belongs to your fleet. }
        - { status: 409, code: no_spot_available, description: "No spot is free at the new time. The reservation is unchanged; `alternatives` lists others nearby with room." }
        - { status: 409, code: staging_area_closed, description: "The staging area is closed at the new time. The reservation is unchanged; `alternatives` lists others nearby with room." }
        - { status: 409, code: reservation_not_held, description: The reservation is no longer held. }
        - { status: 422, code: arrive_by_out_of_range, description: "`arrive_by` is in the past, or more than the staging area's `book_ahead_minutes` ahead." }
  /reservations/{id}/check_in:
    post:
      tags: [Reservations]
      operationId: checkIn
      summary: Check in
      description: |
        The vehicle is in its spot. It can check in any time while the
        reservation is held, early too. From now it may stay until
        `leave_by`: the staging area's `max_stay_minutes` from now, or the
        start of a planned closure of its spot, if that comes first. With
        no limit and no closure planned, `leave_by` is null and the vehicle
        may stay as long as it likes.
      parameters:
        - $ref: "#/components/parameters/ReservationId"
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "200":
          description: The reservation, checked in.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Reservation"
              example:
                id: res_8f2c41
                object: reservation
                vehicle_id: rt-0417
                staging_area: sa_norreport
                spot: "2"
                location: { lat: 55.6838, lng: 12.5721 }
                status: checked_in
                arrive_by: "2026-11-02T14:02:00Z"
                held_until: "2026-11-02T14:07:00Z"
                checked_in_at: "2026-11-02T14:01:37Z"
                leave_by: "2026-11-02T14:46:37Z"
                checked_out_at: null
                cancellation_reason: null
                metadata: {}
                created_at: "2026-11-02T13:51:12Z"
      x-errors:
        - { status: 404, code: not_found, description: No reservation with this id belongs to your fleet. }
        - { status: 409, code: hold_expired, description: "The hold ran out before the vehicle checked in: the reservation is `expired`. Reserve again." }
        - { status: 409, code: reservation_not_held, description: "The reservation is already checked in, checked out or cancelled." }
  /reservations/{id}/check_out:
    post:
      tags: [Reservations]
      operationId: checkOut
      summary: Check out
      description: |
        The vehicle has left its spot. The spot frees up for whoever is
        next. A vehicle past `leave_by` checks out the same way.
      parameters:
        - $ref: "#/components/parameters/ReservationId"
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "200":
          description: The reservation, checked out.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Reservation"
              example:
                id: res_8f2c41
                object: reservation
                vehicle_id: rt-0417
                staging_area: sa_norreport
                spot: "2"
                location: { lat: 55.6838, lng: 12.5721 }
                status: checked_out
                arrive_by: "2026-11-02T14:02:00Z"
                held_until: "2026-11-02T14:07:00Z"
                checked_in_at: "2026-11-02T14:01:37Z"
                leave_by: "2026-11-02T14:46:37Z"
                checked_out_at: "2026-11-02T14:23:05Z"
                cancellation_reason: null
                metadata: {}
                created_at: "2026-11-02T13:51:12Z"
      x-errors:
        - { status: 404, code: not_found, description: No reservation with this id belongs to your fleet. }
        - { status: 409, code: reservation_not_checked_in, description: "The reservation is not checked in: it is still held (cancel it instead), or it has already ended." }
  /reservations/{id}/cancel:
    post:
      tags: [Reservations]
      operationId: cancelReservation
      summary: Cancel a reservation
      description: Gives up a held spot before the vehicle arrives.
      parameters:
        - $ref: "#/components/parameters/ReservationId"
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "200":
          description: The reservation, cancelled.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Reservation"
              example:
                id: res_8f2c41
                object: reservation
                vehicle_id: rt-0417
                staging_area: sa_norreport
                spot: "2"
                location: { lat: 55.6838, lng: 12.5721 }
                status: cancelled
                arrive_by: "2026-11-02T14:02:00Z"
                held_until: "2026-11-02T14:07:00Z"
                checked_in_at: null
                leave_by: null
                checked_out_at: null
                cancellation_reason: requested
                metadata: {}
                created_at: "2026-11-02T13:51:12Z"
      x-errors:
        - { status: 404, code: not_found, description: No reservation with this id belongs to your fleet. }
        - { status: 409, code: reservation_not_held, description: "Only a held reservation can be cancelled. A checked-in vehicle checks out." }

  /webhook_endpoints:
    post:
      tags: [Webhooks]
      operationId: createWebhookEndpoint
      summary: Add a webhook endpoint
      description: |
        Where to send events, and which. The response carries the endpoint's
        signing `secret`, once: store it, you need it to check every event.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WebhookEndpointCreate"
            example:
              url: https://dispatch.example.com/lanoo/events
              events: [reservation.expired, reservation.cancelled, staging_area.closed]
      responses:
        "201":
          description: The endpoint, with its secret.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookEndpoint"
              example:
                id: we_3b91d0
                object: webhook_endpoint
                url: https://dispatch.example.com/lanoo/events
                events: [reservation.expired, reservation.cancelled, staging_area.closed]
                secret: whsec_6f1c0a…
                created_at: "2026-11-01T10:00:00Z"
      x-errors:
        - { status: 422, code: url_invalid, description: "The URL is not HTTPS, or does not resolve." }
    get:
      tags: [Webhooks]
      operationId: listWebhookEndpoints
      summary: List webhook endpoints
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/StartingAfter"
      responses:
        "200":
          description: A page of endpoints. Secrets are not shown again.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookEndpointList"
  /webhook_endpoints/{id}:
    delete:
      tags: [Webhooks]
      operationId: deleteWebhookEndpoint
      summary: Remove a webhook endpoint
      description: No more events are sent to it, including retries still waiting.
      parameters:
        - name: id
          in: path
          required: true
          description: The endpoint's id.
          schema: { type: string, example: we_3b91d0 }
      responses:
        "200":
          description: The endpoint, removed.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string, example: we_3b91d0 }
                  object: { type: string, const: webhook_endpoint }
                  deleted: { type: boolean, const: true }
      x-errors:
        - { status: 404, code: not_found, description: No endpoint with this id. }
    patch:
      tags: [Webhooks]
      operationId: updateWebhookEndpoint
      summary: Change a webhook endpoint
      description: |
        A new URL, other events, or both. Send only what changes. Events
        on their way go to the new URL from the moment it changes, and the
        secret stays the same.
      parameters:
        - name: id
          in: path
          required: true
          description: The endpoint's id.
          schema: { type: string, example: we_3b91d0 }
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WebhookEndpointUpdate"
            example:
              events: [reservation.expired, reservation.cancelled, reservation.overstayed, staging_area.closed]
      responses:
        "200":
          description: The endpoint as it is now.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookEndpoint"
              example:
                id: we_3b91d0
                object: webhook_endpoint
                url: https://dispatch.example.com/lanoo/events
                events: [reservation.expired, reservation.cancelled, reservation.overstayed, staging_area.closed]
                created_at: "2026-11-01T10:00:00Z"
      x-errors:
        - { status: 404, code: not_found, description: No endpoint with this id. }
        - { status: 422, code: url_invalid, description: "The URL is not HTTPS, or does not resolve." }
  /webhook_endpoints/{id}/roll_secret:
    post:
      tags: [Webhooks]
      operationId: rollWebhookSecret
      summary: Get a new secret
      description: |
        For a secret that has leaked, or one you want to change. The
        response carries the new `secret`, once. Choose when the old one
        stops: `now` for a leaked secret, or `in_24_hours` to swap it
        without missing an event. Until the old one stops, every event is
        signed with both.
      parameters:
        - name: id
          in: path
          required: true
          description: The endpoint's id.
          schema: { type: string, example: we_3b91d0 }
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [expire_old_secret]
              properties:
                expire_old_secret:
                  type: string
                  enum: [now, in_24_hours]
                  description: When the old secret stops working.
            example:
              expire_old_secret: in_24_hours
      responses:
        "200":
          description: The endpoint, with its new secret.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookEndpoint"
              example:
                id: we_3b91d0
                object: webhook_endpoint
                url: https://dispatch.example.com/lanoo/events
                events: [reservation.expired, reservation.cancelled, reservation.overstayed, staging_area.closed]
                created_at: "2026-11-01T10:00:00Z"
                secret: whsec_9d2e4b…
      x-errors:
        - { status: 404, code: not_found, description: No endpoint with this id. }
  /webhook_endpoints/{id}/ping:
    post:
      tags: [Webhooks]
      operationId: pingWebhookEndpoint
      summary: Send a test event
      description: |
        Sends a `ping` event to the endpoint straight away, signed like
        every other event, so you can see your server receive and check it.
        It is sent whichever events the endpoint asks for.
      parameters:
        - name: id
          in: path
          required: true
          description: The endpoint's id.
          schema: { type: string, example: we_3b91d0 }
      responses:
        "202":
          description: The event is on its way.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string, description: The test event's id., example: evt_0a4f93 }
                  object: { type: string, const: event }
                  type: { type: string, const: ping }
              example:
                id: evt_0a4f93
                object: event
                type: ping
      x-errors:
        - { status: 404, code: not_found, description: No endpoint with this id. }

components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: |
        Your secret key, as a bearer token. Keys starting `lk_test_` work on
        test staging areas only; `lk_live_` keys book real spots. Test
        staging areas are made up: no vehicles stand in them, they are
        your account's alone, and they work just as real ones do.

  parameters:
    StagingAreaId:
      name: id
      in: path
      required: true
      description: The staging area's id.
      schema: { type: string, example: sa_norreport }
    ReservationId:
      name: id
      in: path
      required: true
      description: The reservation's id.
      schema: { type: string, example: res_8f2c41 }
    Near:
      name: near
      in: query
      description: Search around this point, as `lat,lng`.
      schema: { type: string, example: "55.6812,12.5689" }
    RadiusM:
      name: radius_m
      in: query
      description: How far from `near` to search, in metres. Defaults to 3000.
      schema: { type: integer, minimum: 100, maximum: 20000, default: 3000 }
    Limit:
      name: limit
      in: query
      description: How many to return, 1 to 100. Defaults to 20.
      schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
    StartingAfter:
      name: starting_after
      in: query
      description: For the next page, the id of the last item on this one.
      schema: { type: string }
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      description: |
        A unique key, such as a UUID, for this request. Send the same key
        when you retry: the request takes effect once, and the retry gets the
        first answer back. Kept for 24 hours. The same key with a different
        body is refused with `idempotency_key_reused` (422).
      schema: { type: string, example: 5f0e9a1c-2d7b-4b8e-9c3a-0e6f2a7d1b44 }

  schemas:
    LatLng:
      type: object
      required: [lat, lng]
      properties:
        lat: { type: number, description: "Latitude, WGS 84.", example: 55.6838 }
        lng: { type: number, description: "Longitude, WGS 84.", example: 12.5721 }

    StagingArea:
      type: object
      required: [id, object, name, city, country, location, spots, status, closures, rules, updated_at]
      properties:
        id: { type: string, description: "Unique, starting `sa_`.", example: sa_norreport }
        object: { type: string, const: staging_area }
        name: { type: string, description: What the staging area is called., example: Nørreport }
        city: { type: string, example: Copenhagen }
        country: { type: string, description: "ISO 3166-1 alpha-2.", example: DK }
        location: { $ref: "#/components/schemas/LatLng", description: Where vehicles drive in. }
        spots: { type: integer, description: How many spots it has in all., example: 6 }
        status:
          type: string
          enum: [open, closed]
          description: Whether it takes vehicles right now.
        closures:
          type: array
          description: |
            Planned closures its owner has announced, soonest first. During
            one, the spots it names cannot be reserved or stayed in.
          items: { $ref: "#/components/schemas/Closure" }
        rules: { $ref: "#/components/schemas/Rules" }
        updated_at: { type: string, format: date-time, description: When its details or rules last changed. }
    Closure:
      type: object
      description: |
        A planned closure, for repairs or works, announced ahead by the
        staging area's owner. It never starts before the vehicles already
        checked in there have to leave, so no vehicle is asked to leave for
        one. A sudden closure, by the police or for a fire, is not planned:
        it sets `status` to `closed`; see `reservation.must_leave`.
      required: [starts_at, ends_at, spots, reason]
      example:
        starts_at: "2026-11-09T06:00:00Z"
        ends_at: "2026-11-09T14:00:00Z"
        spots: ["5", "6"]
        reason: Resurfacing
      properties:
        starts_at: { type: string, format: date-time }
        ends_at: { type: [string, "null"], format: date-time, description: "When it ends. Null until the owner knows." }
        spots:
          type: [array, "null"]
          items: { type: string }
          description: The spots it closes, as they are marked. Null when it closes the whole staging area.
          example: ["5", "6"]
        reason: { type: string, description: "Why, in the owner's words. For people.", example: Resurfacing }
    Rules:
      type: object
      description: What the staging area's owner allows. Set in the Lanoo console.
      required: [hold_minutes, max_stay_minutes, book_ahead_minutes, max_vehicle_length_m]
      properties:
        book_ahead_minutes:
          type: integer
          description: "How far from now `arrive_by` may be: a spot can be reserved up to this many minutes ahead."
          example: 120
        hold_minutes:
          type: integer
          description: How long a spot is held past `arrive_by` before it is given up.
          example: 5
        max_stay_minutes:
          type: [integer, "null"]
          description: How long a vehicle may stay once checked in. Null when there is no limit, as at a staging area where vehicles wait out the night.
          example: 45
        max_vehicle_length_m:
          type: [number, "null"]
          description: The longest vehicle allowed, in metres. Null when there is no limit.
          example: 5.2

    Availability:
      type: object
      required: [object, staging_area, name, free_spots, chance_free]
      properties:
        object: { type: string, const: availability }
        staging_area: { type: string, description: The staging area's id., example: sa_norreport }
        name: { type: string, example: Nørreport }
        distance_m: { type: [integer, "null"], description: "Metres from `near`, or, offered as an alternative, from the staging area you asked for. Null when there is neither.", example: 420 }
        free_spots: { type: integer, description: "Spots nobody has reserved at `arrive_at`, or now if you gave no time.", example: 3 }
        chance_free:
          type: number
          minimum: 0
          maximum: 1
          description: "How likely it is that a spot is still free when the vehicle gets there, from 0 to 1. A forecast, not a hold."
          example: 0.92

    ReservationStatus:
      type: string
      enum: [held, checked_in, checked_out, expired, cancelled]
      description: |
        `held`: a spot is kept for the vehicle. `checked_in`: the vehicle is
        in its spot. `checked_out`: it has left. `expired`: it did not arrive
        before `held_until`. `cancelled`: given up before it arrived.
    Reservation:
      type: object
      required: [id, object, vehicle_id, staging_area, spot, location, status, arrive_by, held_until, created_at]
      properties:
        id: { type: string, description: "Unique, starting `res_`.", example: res_8f2c41 }
        object: { type: string, const: reservation }
        vehicle_id: { type: string, description: "Your own name for the vehicle. We only use it to tell your vehicles apart.", example: rt-0417 }
        staging_area: { type: string, description: The staging area's id., example: sa_norreport }
        spot: { type: string, description: "The spot held, as it is marked in the staging area.", example: "2" }
        location: { $ref: "#/components/schemas/LatLng", description: Where the spot is. }
        status: { $ref: "#/components/schemas/ReservationStatus" }
        arrive_by: { type: string, format: date-time, description: When you said the vehicle would arrive. }
        held_until: { type: string, format: date-time, description: "When the hold runs out: `arrive_by` plus the staging area's `hold_minutes`." }
        checked_in_at: { type: [string, "null"], format: date-time }
        leave_by: { type: [string, "null"], format: date-time, description: "Set at check-in: when the vehicle has to leave. Null while it may stay as long as it likes. Moved to the moment it closes if the staging area closes suddenly." }
        checked_out_at: { type: [string, "null"], format: date-time }
        cancellation_reason:
          type: [string, "null"]
          enum: [requested, staging_area_closed, null]
          description: "Why it was cancelled: you asked, or the staging area closed."
        metadata:
          type: object
          additionalProperties: { type: string }
          description: "Your own key-value pairs: up to 20, keys up to 40 characters, values up to 500. We store them and return them; we never read them."
        created_at: { type: string, format: date-time }
    ReservationCreate:
      type: object
      required: [vehicle_id, staging_area, arrive_by]
      properties:
        vehicle_id: { type: string, description: "Your own name for the vehicle, up to 64 characters.", example: rt-0417 }
        staging_area: { type: string, description: The staging area's id., example: sa_norreport }
        arrive_by: { type: string, format: date-time, description: When the vehicle will arrive. }
        metadata:
          type: object
          additionalProperties: { type: string }
          description: "Your own key-value pairs: up to 20, keys up to 40 characters, values up to 500."
        vehicle_length_m:
          type: number
          description: "The vehicle's length in metres. Checked against the staging area's `max_vehicle_length_m`, if it has one."
          example: 4.9
    ReservationUpdate:
      type: object
      required: [arrive_by]
      properties:
        arrive_by: { type: string, format: date-time, description: The new arrival time. }

    Event:
      type: object
      required: [id, object, type, created_at, api_version, data]
      properties:
        id: { type: string, description: "Unique, starting `evt_`. The same event may arrive more than once; use this to notice.", example: evt_1c7e55 }
        object: { type: string, const: event }
        type: { $ref: "#/components/schemas/EventType" }
        created_at: { type: string, format: date-time }
        api_version: { type: string, description: The API version the event is written in., example: "2026-10-30" }
        data:
          type: object
          description: "`object` is the reservation or staging area as it is after the change. On `ping`, it is the endpoint."
          properties:
            object:
              oneOf:
                - $ref: "#/components/schemas/Reservation"
                - $ref: "#/components/schemas/StagingArea"
                - $ref: "#/components/schemas/WebhookEndpoint"
            alternatives:
              type: array
              description: "On `reservation.cancelled` and `reservation.must_leave` when the staging area closed: others nearby with room."
              items: { $ref: "#/components/schemas/Availability" }
    EventType:
      type: string
      enum:
        - reservation.expired
        - reservation.cancelled
        - reservation.overstayed
        - reservation.must_leave
        - staging_area.updated
        - staging_area.closed
        - staging_area.reopened
        - ping
      description: |
        `reservation.expired`: the vehicle did not check in before
        `held_until`. `reservation.cancelled`: the staging area closed and
        gave up your hold; `alternatives` lists others with room. Not sent
        when you cancel yourself. `reservation.overstayed`: a checked-in
        vehicle is past `leave_by`; sent once. What follows is for the
        staging area's owner to decide. `reservation.must_leave`: the
        staging area closed suddenly with your vehicle checked in; it has to
        leave now, and `alternatives` lists others with room.
        `staging_area.updated`: its rules, details or planned closures
        changed.
        `staging_area.closed` and `staging_area.reopened`: it stopped or
        started taking vehicles. `ping`: a test event, sent only when you
        ask for one; you need not list it.
    WebhookEndpoint:
      type: object
      required: [id, object, url, events, created_at]
      properties:
        id: { type: string, description: "Unique, starting `we_`.", example: we_3b91d0 }
        object: { type: string, const: webhook_endpoint }
        url: { type: string, format: uri, description: Where we send events. HTTPS only. }
        events:
          type: array
          items: { $ref: "#/components/schemas/EventType" }
          description: The events sent to it.
        secret: { type: string, description: "For checking signatures. Only in the response that creates the endpoint, or gives it a new secret.", example: whsec_6f1c0a… }
        created_at: { type: string, format: date-time }
    WebhookEndpointUpdate:
      type: object
      properties:
        url: { type: string, format: uri, description: "Where to send events. HTTPS only.", example: https://dispatch.example.com/lanoo/events }
        events:
          type: array
          items: { $ref: "#/components/schemas/EventType" }
          description: Which events to send, in place of the ones before.
    WebhookEndpointCreate:
      type: object
      required: [url, events]
      properties:
        url: { type: string, format: uri, description: "Where to send events. HTTPS only.", example: https://dispatch.example.com/lanoo/events }
        events:
          type: array
          items: { $ref: "#/components/schemas/EventType" }
          description: Which events to send.

    Error:
      type: object
      description: |
        Every error has this shape. The type sets the HTTP status:
        `invalid_request` 400 or 422, `authentication` 401, `not_found` 404,
        `conflict` 409, `rate_limited` 429, `api_error` 500.
      required: [error]
      properties:
        error:
          type: object
          required: [type, code, message]
          properties:
            type:
              type: string
              enum: [invalid_request, authentication, not_found, conflict, rate_limited, api_error]
            code: { type: string, description: "Exact and stable; branch on this, not on `message`.", example: no_spot_available }
            message: { type: string, description: For people. May change. }
            param: { type: string, description: "The field at fault, if one is." }
            alternatives:
              type: array
              description: "With `no_spot_available` and `staging_area_closed`: others nearby with room."
              items: { $ref: "#/components/schemas/Availability" }

    StagingAreaList:
      type: object
      properties:
        object: { type: string, const: list }
        data: { type: array, items: { $ref: "#/components/schemas/StagingArea" } }
        has_more: { type: boolean }
    AvailabilityList:
      type: object
      properties:
        object: { type: string, const: list }
        data: { type: array, items: { $ref: "#/components/schemas/Availability" } }
        has_more: { type: boolean }
    ReservationList:
      type: object
      properties:
        object: { type: string, const: list }
        data: { type: array, items: { $ref: "#/components/schemas/Reservation" } }
        has_more: { type: boolean }
    WebhookEndpointList:
      type: object
      properties:
        object: { type: string, const: list }
        data: { type: array, items: { $ref: "#/components/schemas/WebhookEndpoint" } }
        has_more: { type: boolean }
