openapi: 3.1.0
info:
  title: Travelgenix Contracting API
  version: 1.8.0
  description: >
    Sell a Travelgenix client's directly-contracted product. Eight product
    families share one API and one three-step booking flow (quote, hold,
    confirm): accommodation (hotels, villas and apartments priced for real dates
    and guests), car hire (a contracted fleet priced for pickup/dropoff
    datetimes and a driver age), transfers (a route network priced for a date,
    passengers and direction, one way or return), attraction tickets
    (per-person admission from single-day entry to duration passes), flights
    (block seats and negotiated fares with seat selection), tours (multi-day
    and single-day departures priced per person by room basis), coaches and
    trains (scheduled services priced per seat by pick-up point or boarding
    station). Every family has a bulk priced availability search whose results
    are complete one-call entries: all the bookable options with their rate
    references, the extras and the full product detail, so no per-result
    follow-up calls are needed. The contracting engine is the source of truth
    for product, price and availability. You are the consumer. All prices are
    sell prices; cost and margin are never exposed.
  contact:
    name: Travelgenix
    email: info@travelgenix.io
servers:
  - url: https://contracts.travelify.io/api/v1
    description: Production and sandbox (sandbox is selected by using a sk_test_ key)
security:
  - bearerAuth: []
tags:
  - name: Accommodation
    description: Browse, read and price a client's contracted accommodation (hotels, villas and apartments).
  - name: Car hire
    description: Browse and price a client's contracted car-hire fleet.
  - name: Transfers
    description: Browse and price a client's contracted airport and resort transfers.
  - name: Tickets
    description: Browse, read and price a client's contracted attraction tickets (single-day, multi-day and duration passes).
  - name: Flights
    description: Browse, read and price a client's contracted flight deals (block seats and negotiated fares), with seat selection. No live GDS.
  - name: Tours
    description: Browse, read and price a client's contracted multi-day and single-day tours by departure, with per-person pricing by room basis and live spaces.
  - name: Coaches
    description: Browse, read and price a client's contracted coach and minibus services by departure, with per-seat pricing by pick-up point and live seats.
  - name: Trains
    description: >
      Browse, read and price a client's contracted train services by departure,
      with per-seat pricing by boarding station and live seats. Trains mirror
      coaches: booking is the shared three-step flow (pass a train_ rate_reference
      into /bookings/quote), and search/availability is the same shape at /v1/trains
      with fields renamed for rail - pick_up_points becomes stations, pick_up
      becomes station (boarding_station on the booking), vehicle becomes
      service_class, coach_id becomes train_id, and the bulk pick_up filter becomes
      boarding.
  - name: Booking
    description: The three-step booking lifecycle and booking management, shared across all eight product families.

paths:
  /properties:
    get:
      tags: [Accommodation]
      summary: List accommodation
      description: 'Search the accommodation your key can sell (properties with an active contract). Filter by an id basket, location (country, region/area, city/town, resort, airport), a map radius, property type, star rating, board basis and amenities. No prices here.'
      parameters:
        - { $ref: '#/components/parameters/Ids' }
        - { name: country, in: query, description: 'Country, as the full name or ISO 3166-1 alpha-2 code (e.g. Turkey or TR). Comma-separated for several, e.g. ES,PT.', schema: { type: string } }
        - { name: region, in: query, description: 'Region or area; also matches the resort. Comma-separated.', schema: { type: string } }
        - { name: city, in: query, description: 'City or town. Comma-separated.', schema: { type: string } }
        - { name: resort, in: query, description: 'Resort. Comma-separated.', schema: { type: string } }
        - { name: airport_code, in: query, description: 'IATA code of the nearest airport. Comma-separated.', schema: { type: string } }
        - { name: property_type, in: query, description: 'hotel, villa or apartment. Comma-separated.', schema: { type: string } }
        - { name: board_basis, in: query, description: 'Properties that offer any of these boards: RO,BB,HB,FB,AI. Comma-separated.', schema: { type: string } }
        - { name: amenities, in: query, description: 'Must-have amenities; all must be present. Comma-separated, e.g. pool,wifi.', schema: { type: string } }
        - { name: star_min, in: query, description: Minimum star rating., schema: { type: number } }
        - { name: star_max, in: query, description: Maximum star rating., schema: { type: number } }
        - { $ref: '#/components/parameters/Near' }
        - { name: q, in: query, description: 'Free-text on name, city, resort, region or country.', schema: { type: string } }
        - { $ref: '#/components/parameters/Sort' }
        - { $ref: '#/components/parameters/Page' }
        - { $ref: '#/components/parameters/PageSize' }
      responses:
        '200':
          description: A page of accommodation products.
          headers:
            X-RateLimit-Limit: { $ref: '#/components/headers/RateLimitLimit' }
            X-RateLimit-Remaining: { $ref: '#/components/headers/RateLimitRemaining' }
            X-RateLimit-Reset: { $ref: '#/components/headers/RateLimitReset' }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/AccommodationSummary' }
                  pagination: { $ref: '#/components/schemas/Pagination' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /properties/search:
    post:
      tags: [Accommodation]
      summary: Search accommodation
      description: The same catalogue search as GET /properties, sent as a JSON body so long id baskets and arrays stay clean. No prices here.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PropertySearchRequest' }
      responses:
        '200':
          description: A page of accommodation products.
          headers:
            X-RateLimit-Limit: { $ref: '#/components/headers/RateLimitLimit' }
            X-RateLimit-Remaining: { $ref: '#/components/headers/RateLimitRemaining' }
            X-RateLimit-Reset: { $ref: '#/components/headers/RateLimitReset' }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/AccommodationSummary' }
                  pagination: { $ref: '#/components/schemas/Pagination' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /properties/availability/search:
    post:
      tags: [Accommodation]
      summary: Search availability across products
      description: >
        Priced availability across many properties in one call: the
        PropertySearchRequest filters plus check_in, check_out and occupancy.
        Returns properties with at least one bookable option, each with a lead
        (cheapest) option carrying a rate_reference that feeds the booking flow
        unchanged. Bounded: a scoping filter is required, at most 60 products are
        priced per request, and a product that times out is dropped with
        partial:true on the response. Each result is a one-call entry: every
        bookable room and rate (options) plus the full product content (product),
        so no per-property detail or availability call is needed.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PropertyAvailabilitySearchRequest' }
      responses:
        '200':
          description: A page of priced accommodation results.
          headers:
            X-RateLimit-Limit: { $ref: '#/components/headers/RateLimitLimit' }
            X-RateLimit-Remaining: { $ref: '#/components/headers/RateLimitRemaining' }
            X-RateLimit-Reset: { $ref: '#/components/headers/RateLimitReset' }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/AccommodationAvailabilityResult' }
                  pagination: { $ref: '#/components/schemas/Pagination' }
                  partial: { type: boolean, description: 'True when a product was dropped due to a pricing timeout; refine or retry.' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/Unprocessable' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /properties/{property_id}:
    get:
      tags: [Accommodation]
      summary: Get a property
      description: Full content for one accommodation product - location, amenities, policies and room types. Use this to build the detail page.
      parameters:
        - { $ref: '#/components/parameters/PropertyId' }
      responses:
        '200':
          description: The accommodation product.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Accommodation' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /properties/{property_id}/availability:
    post:
      tags: [Accommodation]
      summary: Price a property
      description: >
        Return bookable, priced room options for one property and dates.
        A room is returned only when it prices for the stay, is not on stop sale,
        has a unit free every night (inventory minus reservations and closures),
        and can take the occupancy sent (its max_adults, max_children and
        max_occupancy caps; a null cap is no cap, max_children 0 is adults only).


        Ask in one of three ways. `occupancy` for one room, exactly as before.
        `rooms` for several rooms where you say who is in each. `party` for a
        group with no room split, where the reply suggests the combinations that
        fit, cheapest first. With `rooms` or `party` each option is a whole
        basket priced as one, with a single rate_reference that quotes and holds
        the group in one go.
      parameters:
        - { $ref: '#/components/parameters/PropertyId' }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/AccommodationAvailabilityRequest' }
            examples:
              family:
                summary: One room
                value:
                  check_in: '2026-07-10'
                  check_out: '2026-07-17'
                  occupancy: { adults: 2, children: [6] }
              two_rooms:
                summary: Two rooms, you say who is in each
                value:
                  check_in: '2026-07-10'
                  check_out: '2026-07-17'
                  rooms:
                    - { adults: 2, children: [6] }
                    - { adults: 2, children: [] }
              whole_party:
                summary: Eight guests, what fits?
                value:
                  check_in: '2026-07-10'
                  check_out: '2026-07-17'
                  party: { adults: 8, children: [] }
      responses:
        '200':
          description: Priced options. An empty options array means nothing is available.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AccommodationAvailabilityResponse' }
        '400':
          description: Malformed request (bad dates or occupancy).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /cars:
    get:
      tags: [Car hire]
      summary: List car products
      description: >
        Browse the car-hire catalogue your key is entitled to sell. A car product is
        one client's contracted fleet at one pickup station. No prices here.
      parameters:
        - { $ref: '#/components/parameters/Ids' }
        - { name: country, in: query, description: 'Country of the pickup station, as the full name or ISO 3166-1 alpha-2 code (e.g. Spain or ES). Comma-separated.', schema: { type: string } }
        - { name: region, in: query, description: 'Region or area of the pickup station. Comma-separated.', schema: { type: string } }
        - { name: city, in: query, description: 'City or town. Comma-separated.', schema: { type: string } }
        - { name: airport_code, in: query, description: 'IATA code of the pickup station airport. Comma-separated.', schema: { type: string } }
        - { name: category, in: query, description: 'Vehicle category, label or ACRISS code, e.g. SUV. Comma-separated.', schema: { type: string } }
        - { name: transmission, in: query, description: 'manual or automatic. Comma-separated.', schema: { type: string } }
        - { name: fuel_type, in: query, description: 'Fuel type. Comma-separated.', schema: { type: string } }
        - { name: seats_min, in: query, description: Minimum number of seats., schema: { type: integer } }
        - { name: air_con, in: query, description: Only products with an air-conditioned vehicle., schema: { type: boolean } }
        - { name: supplier, in: query, description: Supplier name contains., schema: { type: string } }
        - { $ref: '#/components/parameters/Near' }
        - { name: q, in: query, description: 'Free-text supplier, station or city match.', schema: { type: string } }
        - { $ref: '#/components/parameters/Sort' }
        - { $ref: '#/components/parameters/Page' }
        - { $ref: '#/components/parameters/PageSize' }
      responses:
        '200':
          description: A page of car products.
          headers:
            X-RateLimit-Limit: { $ref: '#/components/headers/RateLimitLimit' }
            X-RateLimit-Remaining: { $ref: '#/components/headers/RateLimitRemaining' }
            X-RateLimit-Reset: { $ref: '#/components/headers/RateLimitReset' }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/CarProductSummary' }
                  pagination: { $ref: '#/components/schemas/Pagination' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /cars/{car_id}:
    get:
      tags: [Car hire]
      summary: Get a car product
      description: Full content for one car product. Pickup station, fleet, policies and extras. Use this to build the detail page.
      parameters:
        - { $ref: '#/components/parameters/CarId' }
      responses:
        '200':
          description: The car product.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CarProduct' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /cars/{car_id}/availability:
    post:
      tags: [Car hire]
      summary: Price a car product
      description: >
        Return bookable, priced vehicle options for one car product, a pickup and
        dropoff datetime and a driver age. Rental duration is derived from the two
        datetimes as whole 24-hour periods, rounded up, minimum one, then mapped to a
        length-of-rental tier.
      parameters:
        - { $ref: '#/components/parameters/CarId' }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CarAvailabilityRequest' }
            examples:
              week:
                value:
                  pickup_at: '2026-07-10T10:00:00Z'
                  dropoff_at: '2026-07-17T10:00:00Z'
                  driver_age: 35
      responses:
        '200':
          description: Priced vehicle options. Empty options array if nothing is bookable.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CarAvailabilityResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/Unprocessable' }

  /cars/search:
    post:
      tags: [Car hire]
      summary: Search across car products
      description: >
        Find available, priced car hire for pickup and dropoff datetimes and a
        driver age, narrowed by the same catalogue filters as GET /cars (id
        basket, location, map radius, vehicle attributes). Each result is a
        one-call entry: the lead (cheapest) vehicle, the whole priced fleet
        (options) and the full product detail (product), so no per-product
        follow-up call is needed. A scoping filter (ids, near,
        country, region, city or airport_code) is required, and pricing is
        bounded the same way as the other availability searches: at most a
        capped candidate set is priced, concurrently, under a per-product
        timeout, and a dropped product sets partial:true.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CarSearchRequest' }
      responses:
        '200':
          description: A page of car products with their lead priced vehicle option.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/CarSearchResultItem' }
                  pagination: { $ref: '#/components/schemas/Pagination' }
                  partial: { type: boolean, description: 'True when a product was dropped due to a pricing timeout; refine or retry.' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/Unprocessable' }

  /transfers:
    get:
      tags: [Transfers]
      summary: List transfers
      description: Browse the transfer contracts your key is entitled to sell (active contracts). No prices here.
      parameters:
        - { $ref: '#/components/parameters/Ids' }
        - { name: country, in: query, description: 'Country served by the contract, as the full name or ISO 3166-1 alpha-2 code (e.g. Spain or ES). Comma-separated.', schema: { type: string } }
        - { name: region, in: query, description: 'Region or area served. Comma-separated.', schema: { type: string } }
        - { name: airport_code, in: query, description: 'IATA code of a served point. Comma-separated.', schema: { type: string } }
        - { name: point_type, in: query, description: 'airport, city or resort. Comma-separated.', schema: { type: string } }
        - { name: transfer_mode, in: query, description: 'private or shared. Comma-separated.', schema: { type: string } }
        - { name: vehicle_class, in: query, description: 'Vehicle class. Comma-separated.', schema: { type: string } }
        - { name: pax, in: query, description: Minimum passenger capacity needed., schema: { type: integer } }
        - { name: meet_and_greet, in: query, description: Only contracts that include meet and greet., schema: { type: boolean } }
        - { name: supplier, in: query, description: Supplier name contains., schema: { type: string } }
        - { $ref: '#/components/parameters/Near' }
        - { name: q, in: query, description: 'Free-text supplier, country or region match.', schema: { type: string } }
        - { $ref: '#/components/parameters/Sort' }
        - { $ref: '#/components/parameters/Page' }
        - { $ref: '#/components/parameters/PageSize' }
      responses:
        '200':
          description: A page of transfer products.
          headers:
            X-RateLimit-Limit: { $ref: '#/components/headers/RateLimitLimit' }
            X-RateLimit-Remaining: { $ref: '#/components/headers/RateLimitRemaining' }
            X-RateLimit-Reset: { $ref: '#/components/headers/RateLimitReset' }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/TransferSummary' }
                  pagination: { $ref: '#/components/schemas/Pagination' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /transfers/search:
    post:
      tags: [Transfers]
      summary: Search transfers
      description: The same catalogue search as GET /transfers, sent as a JSON body so long id baskets and arrays stay clean. No prices here.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TransferSearchRequest' }
      responses:
        '200':
          description: A page of transfer products.
          headers:
            X-RateLimit-Limit: { $ref: '#/components/headers/RateLimitLimit' }
            X-RateLimit-Remaining: { $ref: '#/components/headers/RateLimitRemaining' }
            X-RateLimit-Reset: { $ref: '#/components/headers/RateLimitReset' }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/TransferSummary' }
                  pagination: { $ref: '#/components/schemas/Pagination' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /transfers/availability/search:
    post:
      tags: [Transfers]
      summary: Search availability across products
      description: >
        Priced availability across many transfer products in one call: the
        TransferSearchRequest filters plus date, pax, direction and is_return.
        Returns products with at least one qualifying option, each with a lead
        (cheapest qualifying route and vehicle) option carrying a rate_reference.
        route_id is not accepted here. Bounded the same way as the accommodation
        search. Each result is a one-call entry: every priced route and vehicle
        (options), the bookable extras and the full product detail (product), so
        no per-product detail or availability call is needed.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TransferAvailabilitySearchRequest' }
      responses:
        '200':
          description: A page of priced transfer results.
          headers:
            X-RateLimit-Limit: { $ref: '#/components/headers/RateLimitLimit' }
            X-RateLimit-Remaining: { $ref: '#/components/headers/RateLimitRemaining' }
            X-RateLimit-Reset: { $ref: '#/components/headers/RateLimitReset' }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/TransferAvailabilityResult' }
                  pagination: { $ref: '#/components/schemas/Pagination' }
                  partial: { type: boolean, description: 'True when a product was dropped due to a pricing timeout; refine or retry.' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/Unprocessable' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /transfers/{transfer_id}:
    get:
      tags: [Transfers]
      summary: Get a transfer product
      description: Full content for one transfer contract - points, routes, vehicles, extras and policies.
      parameters:
        - { $ref: '#/components/parameters/TransferId' }
      responses:
        '200':
          description: The transfer product.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Transfer' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /transfers/{transfer_id}/availability:
    post:
      tags: [Transfers]
      summary: Price a transfer
      description: >
        Return priced vehicle options for a journey (route, date, passengers, direction
        and one way or return). Honours stop-sales, the sell window, contract validity
        and the booking cutoff; a return is two legs unless a return rate is set. Private
        is priced per vehicle, shared per person.
      parameters:
        - { $ref: '#/components/parameters/TransferId' }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TransferAvailabilityRequest' }
            examples:
              return:
                value:
                  date: '2026-07-10'
                  pax: 3
                  direction: arrival
                  is_return: true
                  return_date: '2026-07-17'
      responses:
        '200':
          description: Priced options. An empty options array means nothing is available.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TransferAvailabilityResponse' }
        '400':
          description: Malformed request (missing date or passengers).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /tickets:
    get:
      tags: [Tickets]
      summary: List attractions
      description: Browse the attraction contracts your key is entitled to sell (active contracts). No prices here.
      parameters:
        - { $ref: '#/components/parameters/Ids' }
        - { name: kind, in: query, description: 'attraction or event. Comma-separated.', schema: { type: string } }
        - { name: country, in: query, description: 'Country served, as the full name or ISO 3166-1 alpha-2 code (e.g. France or FR). Comma-separated.', schema: { type: string } }
        - { name: region, in: query, description: 'Region or area. Comma-separated.', schema: { type: string } }
        - { name: city, in: query, description: 'City or town. Comma-separated.', schema: { type: string } }
        - { name: category, in: query, description: 'Attraction category (e.g. theme_park, museum, sightseeing). Comma-separated.', schema: { type: string } }
        - { name: supplier, in: query, description: Supplier name contains., schema: { type: string } }
        - { $ref: '#/components/parameters/Near' }
        - { name: q, in: query, description: 'Free-text name, supplier, city or country match.', schema: { type: string } }
        - { $ref: '#/components/parameters/Sort' }
        - { $ref: '#/components/parameters/Page' }
        - { $ref: '#/components/parameters/PageSize' }
      responses:
        '200':
          description: A page of attraction products.
          headers:
            X-RateLimit-Limit: { $ref: '#/components/headers/RateLimitLimit' }
            X-RateLimit-Remaining: { $ref: '#/components/headers/RateLimitRemaining' }
            X-RateLimit-Reset: { $ref: '#/components/headers/RateLimitReset' }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/TicketSummary' }
                  pagination: { $ref: '#/components/schemas/Pagination' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /tickets/search:
    post:
      tags: [Tickets]
      summary: Search attractions
      description: The same catalogue search as GET /tickets, sent as a JSON body so long id baskets and arrays stay clean. No prices here.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TicketSearchRequest' }
      responses:
        '200':
          description: A page of attraction products.
          headers:
            X-RateLimit-Limit: { $ref: '#/components/headers/RateLimitLimit' }
            X-RateLimit-Remaining: { $ref: '#/components/headers/RateLimitRemaining' }
            X-RateLimit-Reset: { $ref: '#/components/headers/RateLimitReset' }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/TicketSummary' }
                  pagination: { $ref: '#/components/schemas/Pagination' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /tickets/availability/search:
    post:
      tags: [Tickets]
      summary: Search availability across attractions
      description: >
        Priced availability across many attractions in one call: the
        TicketSearchRequest filters plus an optional visit date. Returns products
        with at least one bookable option, each with a lead (cheapest) option
        carrying a rate_reference. Omit the date to surface only open-dated
        products. Bounded the same way as the other availability searches. Each
        result is a one-call entry: every bookable option priced (options), the
        bookable extras and the full product detail (product), so no per-product
        detail or availability call is needed.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TicketAvailabilitySearchRequest' }
      responses:
        '200':
          description: A page of priced attraction results.
          headers:
            X-RateLimit-Limit: { $ref: '#/components/headers/RateLimitLimit' }
            X-RateLimit-Remaining: { $ref: '#/components/headers/RateLimitRemaining' }
            X-RateLimit-Reset: { $ref: '#/components/headers/RateLimitReset' }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/TicketAvailabilityResult' }
                  pagination: { $ref: '#/components/schemas/Pagination' }
                  partial: { type: boolean, description: 'True when a product was dropped due to a pricing timeout; refine or retry.' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/Unprocessable' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /tickets/{ticket_id}:
    get:
      tags: [Tickets]
      summary: Get an attraction product
      description: Full content for one attraction - its options (with duration and entry rules), ticket types, extras and policies.
      parameters:
        - { $ref: '#/components/parameters/TicketId' }
      responses:
        '200':
          description: The attraction product.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Ticket' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /tickets/{ticket_id}/availability:
    post:
      tags: [Tickets]
      summary: Price an attraction
      description: >
        Return priced options for an attraction on a visit date (or open-dated).
        Each option carries a price-per-ticket list and, when quantities are
        supplied, a basket total. Honours the sell window, contract validity, the
        booking cutoff and stop sales. Carry an option's rate_reference, with the
        chosen tickets, into POST /bookings/quote.
      parameters:
        - { $ref: '#/components/parameters/TicketId' }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TicketAvailabilityRequest' }
            examples:
              basket:
                value:
                  date: '2026-07-10'
                  tickets:
                    - { ticket_type_id: TYPE_ADULT, qty: 2 }
                    - { ticket_type_id: TYPE_CHILD, qty: 1 }
      responses:
        '200':
          description: Priced options. An empty options array means nothing is available.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TicketAvailabilityResponse' }
        '400':
          description: Malformed request.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /flights:
    get:
      tags: [Flights]
      summary: List flight deals
      description: Browse the flight deals your key is entitled to sell (active deals). No prices here.
      parameters:
        - { $ref: '#/components/parameters/Ids' }
        - { name: airline, in: query, description: 'Airline name or IATA code. Comma-separated.', schema: { type: string } }
        - { name: origin, in: query, description: 'Origin airport IATA code. Comma-separated.', schema: { type: string } }
        - { name: dest, in: query, description: 'Destination airport IATA code. Comma-separated.', schema: { type: string } }
        - { name: country, in: query, description: 'Country served. Comma-separated.', schema: { type: string } }
        - { name: city, in: query, description: 'City served. Comma-separated.', schema: { type: string } }
        - { name: supplier, in: query, description: Supplier name contains., schema: { type: string } }
        - { name: q, in: query, description: 'Free-text airline, supplier or airport match.', schema: { type: string } }
        - { $ref: '#/components/parameters/Sort' }
        - { $ref: '#/components/parameters/Page' }
        - { $ref: '#/components/parameters/PageSize' }
      responses:
        '200':
          description: A page of flight deals.
          headers:
            X-RateLimit-Limit: { $ref: '#/components/headers/RateLimitLimit' }
            X-RateLimit-Remaining: { $ref: '#/components/headers/RateLimitRemaining' }
            X-RateLimit-Reset: { $ref: '#/components/headers/RateLimitReset' }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/FlightSummary' }
                  pagination: { $ref: '#/components/schemas/Pagination' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /flights/search:
    post:
      tags: [Flights]
      summary: Search flight deals
      description: The same catalogue search as GET /flights, sent as a JSON body so long id baskets and arrays stay clean. No prices here.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/FlightSearchRequest' }
      responses:
        '200':
          description: A page of flight deals.
          headers:
            X-RateLimit-Limit: { $ref: '#/components/headers/RateLimitLimit' }
            X-RateLimit-Remaining: { $ref: '#/components/headers/RateLimitRemaining' }
            X-RateLimit-Reset: { $ref: '#/components/headers/RateLimitReset' }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/FlightSummary' }
                  pagination: { $ref: '#/components/schemas/Pagination' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /flights/availability/search:
    post:
      tags: [Flights]
      summary: Search availability across flight deals
      description: >
        Priced flight availability for one origin, destination and date across
        many deals in one call. origin, dest, date and at least one adult are
        required, so a search is always bounded to a route. Returns deals with at
        least one bookable journey, each with a lead (cheapest) option carrying a
        rate_reference. Add return_date for a return trip. Each result is a
        one-call entry: every bookable cabin and routing priced (options) plus
        the full deal detail (product), so no per-deal follow-up call is needed.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/FlightAvailabilitySearchRequest' }
      responses:
        '200':
          description: A page of priced flight results.
          headers:
            X-RateLimit-Limit: { $ref: '#/components/headers/RateLimitLimit' }
            X-RateLimit-Remaining: { $ref: '#/components/headers/RateLimitRemaining' }
            X-RateLimit-Reset: { $ref: '#/components/headers/RateLimitReset' }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/FlightAvailabilityResult' }
                  pagination: { $ref: '#/components/schemas/Pagination' }
                  partial: { type: boolean, description: 'True when a deal was dropped due to a pricing timeout; refine or retry.' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /flights/{flight_id}:
    get:
      tags: [Flights]
      summary: Get a flight deal
      description: Full content for one deal - its airports, routes (with legs and schedules), aircraft (with seat-map summaries), the cabins each route sells, extras and policies.
      parameters:
        - { $ref: '#/components/parameters/FlightId' }
      responses:
        '200':
          description: The flight deal.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Flight' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /flights/{flight_id}/availability:
    post:
      tags: [Flights]
      summary: Price a flight deal
      description: >
        Return priced journeys for a deal on a route and date, one-way or return,
        for a passenger mix. Each option carries a per-passenger fare breakdown,
        seat availability and a rate_reference. Honours the sell window, travel
        validity and the ticketing cutoff. Carry an option's rate_reference, with
        the passengers, into POST /bookings/quote.
      parameters:
        - { $ref: '#/components/parameters/FlightId' }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/FlightAvailabilityRequest' }
            examples:
              return:
                value:
                  origin: LHR
                  dest: JFK
                  date: '2026-07-10'
                  return_date: '2026-07-17'
                  adults: 2
                  children: 1
                  cabin: Y
      responses:
        '200':
          description: Priced options. An empty options array means nothing is available.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/FlightAvailabilityResponse' }
        '400':
          description: Malformed request.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /flights/{flight_id}/seatmap:
    post:
      tags: [Flights]
      summary: Get the seat map
      description: >
        The seat grid for the seat picker, one block per flown segment. Send a
        rate_reference (it pins the route, cabin and dates), or route_id + date
        (plus optional cabin, return_route_id and return_date). Each seat carries
        its type, attributes, any paid-seat fee, and whether it is available
        (false when blocked or already taken).
      parameters:
        - { $ref: '#/components/parameters/FlightId' }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/FlightSeatmapRequest' }
      responses:
        '200':
          description: The seat map per segment.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/FlightSeatmapResponse' }
        '400':
          description: Malformed request.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /tours:
    get:
      tags: [Tours]
      summary: List tours
      description: Browse the multi-day and single-day tours your key can sell (active). No prices per departure here.
      parameters:
        - { $ref: '#/components/parameters/Ids' }
        - { name: country, in: query, description: 'Country, as the full name or ISO 3166-1 alpha-2 code. Comma-separated.', schema: { type: string } }
        - { name: region, in: query, description: 'Region or area. Comma-separated.', schema: { type: string } }
        - { name: city, in: query, description: 'City or town. Comma-separated.', schema: { type: string } }
        - { name: category, in: query, description: 'Tour style (e.g. guided, escorted, day tour). Comma-separated.', schema: { type: string } }
        - { name: supplier, in: query, description: Operator name contains., schema: { type: string } }
        - { $ref: '#/components/parameters/Near' }
        - { name: q, in: query, description: 'Free-text name, operator, city or country match.', schema: { type: string } }
        - { $ref: '#/components/parameters/Sort' }
        - { $ref: '#/components/parameters/Page' }
        - { $ref: '#/components/parameters/PageSize' }
      responses:
        '200':
          description: A page of tours.
          headers:
            X-RateLimit-Limit: { $ref: '#/components/headers/RateLimitLimit' }
            X-RateLimit-Remaining: { $ref: '#/components/headers/RateLimitRemaining' }
            X-RateLimit-Reset: { $ref: '#/components/headers/RateLimitReset' }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/TourSummary' }
                  pagination: { $ref: '#/components/schemas/Pagination' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /tours/search:
    post:
      tags: [Tours]
      summary: Search tours
      description: The same catalogue search as GET /tours, sent as a JSON body. No prices per departure here.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TourSearchRequest' }
      responses:
        '200':
          description: A page of tours.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/TourSummary' }
                  pagination: { $ref: '#/components/schemas/Pagination' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /tours/availability/search:
    post:
      tags: [Tours]
      summary: Search availability across tours
      description: >
        Priced availability across many tours in one call: the TourSearchRequest
        filters plus an optional departure window (date_from defaults to today)
        and traveller basket. Returns tours with at least one bookable departure
        in the window. Each result is a one-call entry: up to 10 departures
        soonest first, each with its room bases priced (every rate_reference
        feeds POST /bookings/quote unchanged), the total bookable
        departure_count, the lead (cheapest) room basis, the bookable extras and
        the full product detail (product), so no per-tour detail or availability
        call is needed. Departures that are not bookable, or without enough
        spaces for the basket, are excluded. Bounded the same way as the other
        availability searches.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TourAvailabilitySearchRequest' }
      responses:
        '200':
          description: A page of priced tour results.
          headers:
            X-RateLimit-Limit: { $ref: '#/components/headers/RateLimitLimit' }
            X-RateLimit-Remaining: { $ref: '#/components/headers/RateLimitRemaining' }
            X-RateLimit-Reset: { $ref: '#/components/headers/RateLimitReset' }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/TourAvailabilityResult' }
                  pagination: { $ref: '#/components/schemas/Pagination' }
                  partial: { type: boolean, description: 'True when a product was dropped due to a pricing timeout; refine or retry.' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/Unprocessable' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /tours/{tour_id}:
    get:
      tags: [Tours]
      summary: Get a tour
      description: Full content for one tour - room bases, traveller types, seasons, the itinerary, inclusions, deposit and cancellation policy, and every departure with its date, status, capacity and spaces left.
      parameters:
        - { name: tour_id, in: path, required: true, schema: { type: string } }
      responses:
        '200':
          description: The tour.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Tour' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /tours/{tour_id}/availability:
    post:
      tags: [Tours]
      summary: Price a departure
      description: >
        Return the room bases at the sell price per person for one departure, for a
        traveller basket. Pin the departure by departure_id or departure_date.
        Honours the sell window, the booking cutoff, the departure status and the
        spaces left. Carry a room basis rate_reference, with the chosen travellers,
        into POST /bookings/quote.
      parameters:
        - { name: tour_id, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TourAvailabilityRequest' }
            examples:
              basket:
                value:
                  departure_date: '2026-07-04'
                  travellers:
                    - { ticket_type_id: TYPE_ADULT, qty: 2 }
      responses:
        '200':
          description: The departure and its priced room bases. A reason is set when nothing is bookable.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TourAvailabilityResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /coaches:
    get:
      tags: [Coaches]
      summary: List coach routes
      description: >
        Browse the active coach and minibus routes your key can sell. By default each
        result is a light summary. Pass `include` to embed the full route detail on
        every result, so a browse takes one call instead of a follow-up GET
        /coaches/{coach_id} per route.
      parameters:
        - { $ref: '#/components/parameters/Ids' }
        - { name: country, in: query, description: 'Country, full name or ISO 3166-1 alpha-2 code. Comma-separated.', schema: { type: string } }
        - { name: region, in: query, description: 'Region or area. Comma-separated.', schema: { type: string } }
        - { name: city, in: query, description: 'City or town. Comma-separated.', schema: { type: string } }
        - { name: operator, in: query, description: Operator name contains., schema: { type: string } }
        - { $ref: '#/components/parameters/Near' }
        - { name: q, in: query, description: 'Free-text name, operator, destination or city match.', schema: { type: string } }
        - name: include
          in: query
          description: >
            Embed detail sections on each result, comma-separated. Use `all` for
            everything, or pick from `pick_up_points`, `passenger_types`, `seasons`
            and `departures` (departures carry their `sailings`). `sailings` and
            `pickups` are accepted as aliases. Only the current page is expanded, so
            page through large catalogues.
          schema: { type: string, example: all }
        - { $ref: '#/components/parameters/Sort' }
        - { $ref: '#/components/parameters/Page' }
        - { $ref: '#/components/parameters/PageSize' }
      responses:
        '200':
          description: A page of coach routes.
          headers:
            X-RateLimit-Limit: { $ref: '#/components/headers/RateLimitLimit' }
            X-RateLimit-Remaining: { $ref: '#/components/headers/RateLimitRemaining' }
            X-RateLimit-Reset: { $ref: '#/components/headers/RateLimitReset' }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/CoachSummary' }
                  pagination: { $ref: '#/components/schemas/Pagination' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /coaches/search:
    post:
      tags: [Coaches]
      summary: Search coach routes
      description: The same catalogue search as GET /coaches, sent as a JSON body. No prices per departure here.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CoachSearchRequest' }
      responses:
        '200':
          description: A page of coach routes.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/CoachSummary' }
                  pagination: { $ref: '#/components/schemas/Pagination' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /coaches/{coach_id}:
    get:
      tags: [Coaches]
      summary: Get a coach route
      description: Full content for one coach route - pick-up points (with board times), passenger types, seasons, and every departure with its date, status, capacity and seats left.
      parameters:
        - { name: coach_id, in: path, required: true, schema: { type: string } }
      responses:
        '200':
          description: The coach route.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Coach' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /coaches/{coach_id}/availability:
    post:
      tags: [Coaches]
      summary: Price a departure
      description: >
        Return the pick-up points at the sell price per seat for one departure, for
        a passenger basket. Pin the departure by departure_id or departure_date.
        Honours the sell window, the booking cutoff, the departure status and the
        seats left. Carry a pick-up rate_reference, with the chosen passengers, into
        POST /bookings/quote.
      parameters:
        - { name: coach_id, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CoachAvailabilityRequest' }
            examples:
              basket:
                value:
                  departure_date: '2026-07-04'
                  passengers:
                    - { passenger_type_id: TYPE_ADULT, qty: 2 }
      responses:
        '200':
          description: The departure and its priced pick-up points. A reason is set when nothing is bookable.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CoachAvailabilityResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /coaches/{coach_id}/seatmap:
    post:
      tags: [Coaches]
      summary: Get the seat map
      description: >
        The seat plan for one departure, for the seat picker. Pin the departure by
        departure_id or departure_date. Returns the seat types and every seat with
        its position, surcharge and whether it is available (false when blocked or
        already taken). Empty when the route has no seat plan (it is then sold by
        seat count only). Carry the chosen seats into POST /bookings/hold.
      parameters:
        - { name: coach_id, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TicketSeatmapRequest' }
            examples:
              byDate:
                value: { departure_date: '2026-07-04' }
      responses:
        '200':
          description: The departure, the seat types and every seat.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CoachSeatmapResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /coaches/availability:
    post:
      tags: [Coaches]
      summary: Search availability across routes
      description: >
        Search bookable coach departures across ALL routes in one call, by pick-up
        and/or destination geo radius, a travel date and a passenger party. Returns
        a list of matching routes, each with the bookable pick-up points (within the
        pick-up radius) priced for the party, the seats left, and a rate_reference
        per pick-up for POST /bookings/quote, plus the full route detail (product)
        so no per-route follow-up call is needed. Routes without enough seats for
        the party, or with no service on the date, are excluded. Direction-aware:
        the pick_up filter matches where the passenger boards and destination where
        they alight. Use this instead of calling /coaches/{id}/availability per route.


        CRUISE FEEDERS. Send `sailing_id` instead of a date and the answer is
        exactly two results: the outbound coach for that sailing and the inbound
        coach for that same sailing, each priced on its own with its own
        rate_reference. No other cruises, no other dates, no other routes. Each
        result carries `leg` ("outbound" for the leg the sailing_id names,
        "inbound" for its return), and the response carries a `sailing` object.
        The inbound leg comes from the sailing's OWN return date rather than the
        coach date, because one run can feed two cruises that come home on
        different days and a date alone cannot tell them apart. `sailing_id`
        comes from any departure's `sailings[].sailing_id`.


        To narrow an ordinary date search to one cruise instead, send `sailing`
        with the sailing name, ship or voyage reference.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CoachAvailabilitySearchRequest' }
            examples:
              homeToPort:
                value:
                  date: '2026-07-04'
                  pick_up: { lat: 51.48, lon: -3.18, radius_km: 30 }
                  destination: { lat: 50.90, lon: -1.40, radius_km: 15 }
                  passengers: { adults: 2, children: 1 }
              oneCruiseBothLegs:
                summary: One cruise, its two coach legs, nothing else
                value:
                  sailing_id: sail_eyJ2IjoxLCJrIjoic2FpbCJ9
                  passengers: { adults: 2, children: 1 }
                  pick_up: { lat: 51.48, lon: -3.18, radius_km: 30 }
      responses:
        '200':
          description: A page of matching routes with priced, bookable pick-up points.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/CoachAvailabilityResult' }
                  pagination: { $ref: '#/components/schemas/Pagination' }
                  sailing:
                    description: 'Present only on a sailing_id search: the cruise both legs belong to.'
                    type: object
                    properties:
                      sailing_id: { type: string }
                      name: { type: string }
                      ship: { type: string, nullable: true }
                      reference: { type: string, nullable: true }
                      outbound_date: { type: string, format: date }
                      return_date: { type: string, format: date, nullable: true }
                  inbound_unavailable:
                    type: string
                    description: >
                      Present only on a sailing_id search that returned one leg,
                      saying why there is no coach home: no_return_date,
                      no_return_route, or no_return_departure_on_YYYY-MM-DD.
                  priced_for:
                    type: object
                    properties:
                      date: { type: string, format: date }
                      passengers: {}
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /trains:
    get:
      tags: [Trains]
      summary: List train services
      description: Browse the active train services your key can sell. No prices per departure here.
      parameters:
        - { $ref: '#/components/parameters/Ids' }
        - { name: country, in: query, description: 'Country, full name or ISO 3166-1 alpha-2 code. Comma-separated.', schema: { type: string } }
        - { name: region, in: query, description: 'Region or area. Comma-separated.', schema: { type: string } }
        - { name: city, in: query, description: 'City or town. Comma-separated.', schema: { type: string } }
        - { name: operator, in: query, description: Operator name contains., schema: { type: string } }
        - { $ref: '#/components/parameters/Near' }
        - { name: q, in: query, description: 'Free-text name, operator, destination or city match.', schema: { type: string } }
        - { $ref: '#/components/parameters/Sort' }
        - { $ref: '#/components/parameters/Page' }
        - { $ref: '#/components/parameters/PageSize' }
      responses:
        '200':
          description: A page of train services.
          headers:
            X-RateLimit-Limit: { $ref: '#/components/headers/RateLimitLimit' }
            X-RateLimit-Remaining: { $ref: '#/components/headers/RateLimitRemaining' }
            X-RateLimit-Reset: { $ref: '#/components/headers/RateLimitReset' }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/TrainSummary' }
                  pagination: { $ref: '#/components/schemas/Pagination' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /trains/search:
    post:
      tags: [Trains]
      summary: Search train services
      description: The same catalogue search as GET /trains, sent as a JSON body. No prices per departure here.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TrainSearchRequest' }
      responses:
        '200':
          description: A page of train services.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/TrainSummary' }
                  pagination: { $ref: '#/components/schemas/Pagination' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /trains/{train_id}:
    get:
      tags: [Trains]
      summary: Get a train service
      description: Full content for one train service - boarding stations (with times), passenger types, seasons, and every departure with its date, status, capacity and seats left.
      parameters:
        - { name: train_id, in: path, required: true, schema: { type: string } }
      responses:
        '200':
          description: The train service.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Train' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /trains/{train_id}/availability:
    post:
      tags: [Trains]
      summary: Price a departure
      description: >
        Return the boarding stations at the sell price per seat for one departure, for
        a passenger basket. Pin the departure by departure_id or departure_date.
        Honours the sell window, the booking cutoff, the departure status and the
        seats left. Carry a station rate_reference, with the chosen passengers, into
        POST /bookings/quote.
      parameters:
        - { name: train_id, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TrainAvailabilityRequest' }
            examples:
              basket:
                value:
                  departure_date: '2026-07-04'
                  passengers:
                    - { passenger_type_id: TYPE_ADULT, qty: 2 }
      responses:
        '200':
          description: The departure and its priced boarding stations. A reason is set when nothing is bookable.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TrainAvailabilityResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /trains/{train_id}/seatmap:
    post:
      tags: [Trains]
      summary: Get the seat map
      description: >
        The seat plan for one departure, for the seat picker. Pin the departure by
        departure_id or departure_date. Returns the seat types and every seat with
        its position, surcharge and whether it is available (false when blocked or
        already taken). Empty when the service has no seat plan (it is then sold by
        seat count only). Carry the chosen seats into POST /bookings/hold.
      parameters:
        - { name: train_id, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TicketSeatmapRequest' }
            examples:
              byDate:
                value: { departure_date: '2026-07-04' }
      responses:
        '200':
          description: The departure, the seat types and every seat.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TrainSeatmapResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /trains/availability:
    post:
      tags: [Trains]
      summary: Search availability across services
      description: >
        Search bookable train departures across ALL services in one call, by boarding
        and/or destination geo radius, a travel date and a passenger party. Returns
        a list of matching services, each with the bookable stations (within the
        boarding radius) priced for the party, the seats left, and a rate_reference
        per station for POST /bookings/quote, plus the full service detail (product)
        so no per-service follow-up call is needed. Services without enough seats
        for the party, or with no service on the date, are excluded. Direction-aware:
        the boarding filter matches where the passenger boards and destination where
        they alight. Use this instead of calling /trains/{id}/availability per service.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TrainAvailabilitySearchRequest' }
            examples:
              coastToCity:
                value:
                  date: '2026-07-04'
                  boarding: { lat: 50.90, lon: -1.41, radius_km: 30 }
                  destination: { lat: 51.50, lon: -0.11, radius_km: 15 }
                  passengers: { adults: 2, children: 1 }
      responses:
        '200':
          description: A page of matching services with priced, bookable stations.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/TrainAvailabilityResult' }
                  pagination: { $ref: '#/components/schemas/Pagination' }
                  priced_for:
                    type: object
                    properties:
                      date: { type: string, format: date }
                      passengers: { type: object }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /bookings/quote:
    post:
      tags: [Booking]
      summary: Quote (step 1)
      description: >
        Turn a rate_reference into a firm, time-limited price. No inventory is reserved.
        The same call serves all eight product families; the engine knows from the
        rate_reference which product it is, and the quote echoes the matching fields.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/QuoteRequest' }
      responses:
        '200':
          description: A short-lived quote.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Quote' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '409': { $ref: '#/components/responses/Conflict' }
        '422': { $ref: '#/components/responses/Unprocessable' }

  /bookings/hold:
    post:
      tags: [Booking]
      summary: Hold (step 2)
      description: >
        Reserve the booking and attach the traveller. Creates a held booking and starts
        the hold clock. Send an Idempotency-Key so a retry never creates two bookings.
        For accommodation send lead_guest and guests. For car hire send lead_guest, a
        driver and any prepaid extras.
      parameters:
        - { $ref: '#/components/parameters/IdempotencyKey' }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/HoldRequest' }
      responses:
        '200':
          description: A held booking.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Booking' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '409': { $ref: '#/components/responses/Conflict' }
        '422': { $ref: '#/components/responses/Unprocessable' }

  /bookings/{booking_id}/confirm:
    post:
      tags: [Booking]
      summary: Confirm (step 3)
      description: Commit a held booking once you have taken payment. You are the merchant of record.
      parameters:
        - { $ref: '#/components/parameters/BookingId' }
        - { $ref: '#/components/parameters/IdempotencyKey' }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ConfirmRequest' }
      responses:
        '200':
          description: The confirmed booking.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Booking' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }

  /bookings/{booking_id}/voucher:
    get:
      tags: [Booking]
      summary: Booking voucher (PDF)
      description: 'The booking''s voucher as a PDF, white-labelled to your brand. The same content as the shareable voucher_url carried on the booking. Add ?download=1 to force a download rather than inline display.'
      parameters:
        - { $ref: '#/components/parameters/BookingId' }
      responses:
        '200':
          description: The voucher PDF.
          content:
            application/pdf:
              schema: { type: string, format: binary }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /bookings/{booking_id}:
    get:
      tags: [Booking]
      summary: Retrieve a booking
      parameters:
        - { $ref: '#/components/parameters/BookingId' }
      responses:
        '200':
          description: The booking.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Booking' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
    patch:
      tags: [Booking]
      summary: Amend a booking
      description: >
        v1 amends contact and notes only. For accommodation you may also amend guest
        details; for car hire you may also amend the driver. Date, vehicle, room or
        occupancy changes are done by cancelling and rebooking.
      parameters:
        - { $ref: '#/components/parameters/BookingId' }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/AmendRequest' }
      responses:
        '200':
          description: The updated booking.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Booking' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/Unprocessable' }

  /bookings:
    get:
      tags: [Booking]
      summary: Find a booking by reference
      parameters:
        - { name: reference, in: query, schema: { type: string } }
        - { name: external_ref, in: query, schema: { type: string } }
      responses:
        '200':
          description: Matching bookings.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Booking' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /bookings/{booking_id}/cancel:
    post:
      tags: [Booking]
      summary: Cancel a booking
      description: Applies the cancellation policy as at today and returns the sell-side charge and refund.
      parameters:
        - { $ref: '#/components/parameters/BookingId' }
      requestBody:
        required: false
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CancelRequest' }
      responses:
        '200':
          description: The cancelled booking with charge and refund breakdown.
          content:
            application/json:
              schema:
                allOf:
                  - { $ref: '#/components/schemas/Booking' }
                  - type: object
                    properties:
                      cancellation:
                        type: object
                        properties:
                          charge: { $ref: '#/components/schemas/Money' }
                          refund: { $ref: '#/components/schemas/Money' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: An API key (sk_live_ or sk_test_) sent as a bearer token.

  parameters:
    Page:
      name: page
      in: query
      schema: { type: integer, minimum: 1, default: 1 }
    PageSize:
      name: page_size
      in: query
      schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
    Ids:
      name: ids
      in: query
      description: 'Fetch a specific set of products by id, comma-separated. Combine with other filters, or use alone as a basket fetch.'
      schema: { type: string }
    Near:
      name: near
      in: query
      description: 'Map radius as "lat,lon,radius_km" (radius defaults to 25). Returns products within the radius, nearest first; each result carries distance_km.'
      schema: { type: string }
    Sort:
      name: sort
      in: query
      description: 'Order: name (default), distance (the default when near is set), or rating (accommodation only).'
      schema: { type: string, enum: [name, distance, rating] }
    ProductId:
      name: product_id
      in: path
      required: true
      schema: { type: string }
    PropertyId:
      name: property_id
      in: path
      required: true
      schema: { type: string }
      description: An accommodation product id (a property).
    TransferId:
      name: transfer_id
      in: path
      required: true
      schema: { type: string }
      description: A transfer product id (a contracted route network).
    TicketId:
      name: ticket_id
      in: path
      required: true
      schema: { type: string }
      description: An attraction product id.
    FlightId:
      name: flight_id
      in: path
      required: true
      schema: { type: string }
      description: A flight product id (a contracted airline deal).
    CarId:
      name: car_id
      in: path
      required: true
      schema: { type: string }
      description: A car product id (a contracted fleet at a pickup station).
    BookingId:
      name: booking_id
      in: path
      required: true
      schema: { type: string }
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema: { type: string }
      description: A unique string (a UUID is ideal). Replaying the same key with the same body returns the original result.

  headers:
    RateLimitLimit:
      description: Requests allowed per window.
      schema: { type: integer }
    RateLimitRemaining:
      description: Requests remaining in the current window.
      schema: { type: integer }
    RateLimitReset:
      description: Unix time when the window resets.
      schema: { type: integer }

  responses:
    Unauthorized:
      description: Missing or invalid key.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    NotFound:
      description: The resource does not exist, or your key is not entitled to it.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Conflict:
      description: Price or availability moved since you read it. Re-price and retry.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Unprocessable:
      description: Well-formed but cannot be fulfilled.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    RateLimited:
      description: Over your request budget. Honour Retry-After.
      headers:
        Retry-After:
          description: Seconds to wait before retrying.
          schema: { type: integer }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    BadRequest:
      description: Malformed request, or no scoping filter was supplied on a bulk search.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }

  schemas:
    PropertyType:
      type: string
      description: An accommodation product type.
      enum: [hotel, villa, apartment]

    BookableType:
      type: string
      description: The product family a booking belongs to.
      enum: [accommodation, car_hire, transfer, attraction, event, tour, coach, flight]

    BoardBasis:
      type: string
      description: Room only, bed and breakfast, half board, full board, all inclusive.
      enum: [RO, BB, HB, FB, AI]

    BookingStatus:
      type: string
      enum: [held, confirmed, cancelled, expired]

    Money:
      type: object
      required: [amount, currency]
      properties:
        amount: { type: number, example: 1043.00 }
        currency: { type: string, minLength: 3, maxLength: 3, example: EUR }

    Price:
      type: object
      required: [amount, currency, per]
      properties:
        amount: { type: number, example: 1850.00 }
        currency: { type: string, minLength: 3, maxLength: 3, example: EUR }
        per:
          type: string
          description: >
            What the amount covers. accommodation: stay (the total for the stay).
            car hire: rental (the total for the rental). transfers: one_way or return
            (the total for the journey). coaches and trains: seat. tours: person.
            attractions and events: admission.
          enum: [stay, week, night, rental, one_way, return, seat, person, admission]

    Pagination:
      type: object
      properties:
        page: { type: integer, example: 1 }
        page_size: { type: integer, example: 20 }
        total: { type: integer, example: 1 }
        has_more: { type: boolean, example: false }

    Occupancy:
      type: object
      required: [adults]
      properties:
        adults: { type: integer, minimum: 1, example: 2 }
        children:
          type: array
          description: One age per child. An empty array means no children.
          items: { type: integer }
          example: [6]

    Airport:
      type: object
      properties:
        code: { type: string, example: FAO }
        name: { type: string, example: Faro }
        distance_km: { type: number, example: 38 }

    Location:
      type: object
      properties:
        latitude: { type: number, example: 37.0891 }
        longitude: { type: number, example: -8.2476 }
        address: { type: string }
        nearest_airport: { $ref: '#/components/schemas/Airport' }
        beach_distance_m: { type: integer, example: 120 }

    TouristTax:
      type: object
      properties:
        amount: { type: number }
        currency: { type: string }
        per: { type: string, example: person_per_night }
        max_nights: { type: integer }

    Policies:
      type: object
      properties:
        check_in_time: { type: string, example: '15:00' }
        check_out_time: { type: string, example: '11:00' }
        min_checkin_age: { type: integer, example: 18 }
        tourist_tax: { $ref: '#/components/schemas/TouristTax' }

    Room:
      type: object
      properties:
        id: { type: string }
        name: { type: string, example: Twin Garden View }
        max_occupancy: { type: integer, example: 3 }
        max_adults: { type: integer, example: 2 }
        max_children: { type: integer, example: 2 }
        size_sqm: { type: number, example: 24 }
        bedding: { type: string, example: 2 single or 1 double }
        facilities:
          type: array
          items: { type: string }
        images:
          type: array
          items: { type: string, format: uri }

    ProductSummary:
      type: object
      properties:
        id: { type: string, example: d865cac7-... }
        type: { $ref: '#/components/schemas/PropertyType' }
        name: { type: string, example: Vale do Sol Beach Hotel }
        star_rating: { type: integer, nullable: true, example: 4 }
        country: { type: string, example: Portugal }
        country_code: { type: string, nullable: true, example: PT, description: 'ISO 3166-1 alpha-2, derived from the country. Null when unrecognised.' }
        city: { type: string, example: Albufeira }
        currency: { type: string, example: EUR }
        hero_image: { type: string, format: uri }
        location: { $ref: '#/components/schemas/Location' }

    Product:
      allOf:
        - { $ref: '#/components/schemas/ProductSummary' }
        - type: object
          properties:
            description: { type: string }
            facilities:
              type: array
              items: { type: string }
            images:
              type: array
              items: { type: string, format: uri }
            policies: { $ref: '#/components/schemas/Policies' }
            rooms:
              type: array
              items: { $ref: '#/components/schemas/Room' }
            extras:
              type: array
              description: 'Optional items sold alongside the room (a cot, a late check-out, a board upgrade). Present only when the property has any. Send the ones you want at hold; a mandatory extra is already inside the quoted price and a pay-local extra is settled at the property, and both are rejected if sent.'
              items: { $ref: '#/components/schemas/AccommodationExtra' }
            bedrooms: { type: integer, description: villa and apartment only }
            bathrooms: { type: number, description: villa and apartment only }
            security_deposit:
              description: villa and apartment only
              oneOf:
                - { $ref: '#/components/schemas/Money' }
                - { type: 'null' }

    AccommodationExtra:
      type: object
      description: An optional item an agent sells alongside the room.
      properties:
        id: { type: string, description: 'Send this as extra_id at hold.' }
        name: { type: string, example: Cot }
        type: { type: string, nullable: true, example: cot, description: 'cot, extra_bed, early_check_in, late_check_out, board_upgrade, breakfast, parking, airport_transfer, welcome_pack, spa or other.' }
        charge_basis:
          type: string
          enum: [per_stay, per_night, per_person, per_person_per_night, per_unit, per_unit_per_night, pay_locally]
          description: 'How the amount multiplies up. per_person counts adults plus children; per_unit multiplies by the quantity you send and nothing else.'
        price: { $ref: '#/components/schemas/Money' }
        payable:
          type: string
          enum: [prepaid, local]
          description: 'prepaid is taken with the booking. local is quoted for information and settled at the property, and is rejected if sent at hold.'
        mandatory: { type: boolean, description: 'Already inside the quoted room price. Rejected if sent at hold.' }
        max_qty: { type: integer, description: 'The most that may be booked. Omitted when there is no limit.' }
        notes: { type: string }

    CancellationBand:
      type: object
      properties:
        from_days_before: { type: integer, example: 22 }
        charge_pct: { type: number, example: 0 }

    BreakdownLine:
      type: object
      properties:
        label: { type: string, example: Room, 7 nights B&B }
        amount: { type: number, example: 980.00 }

    Offer:
      type: object
      properties:
        name: { type: string, example: Early booking 10% }
        applied: { type: boolean, example: true }
        saving: { type: number, example: 116.00 }

    StayRules:
      type: object
      properties:
        changeover_day: { type: string, example: Saturday }
        min_nights: { type: integer, example: 7 }

    SecurityDeposit:
      type: object
      description: A refundable deposit. For car hire it is a card pre-authorisation that is never charged unless there is damage or loss.
      properties:
        amount: { type: number, example: 500.00 }
        currency: { type: string, example: EUR }
        refundable: { type: boolean, example: true }
        method: { type: string, description: car hire only, example: card_pre_authorisation }

    Option:
      type: object
      description: A priced, bookable accommodation result. Hotels populate room_id and board_basis; villas and apartments populate unit_name and stay_rules.
      properties:
        rate_reference: { type: string, description: Carry this into POST /bookings/quote }
        room_id: { type: string }
        room_name: { type: string }
        unit_name: { type: string }
        board_basis: { $ref: '#/components/schemas/BoardBasis' }
        occupancy: { $ref: '#/components/schemas/Occupancy' }
        price: { $ref: '#/components/schemas/Price' }
        breakdown:
          type: array
          items: { $ref: '#/components/schemas/BreakdownLine' }
        offer: { $ref: '#/components/schemas/Offer' }
        security_deposit: { $ref: '#/components/schemas/SecurityDeposit' }
        stay_rules: { $ref: '#/components/schemas/StayRules' }
        cancellation_policy:
          type: array
          items: { $ref: '#/components/schemas/CancellationBand' }
        available_units: { type: integer, description: hotels only }
        available: { type: boolean, description: villas and apartments only }

    AvailabilityRequest:
      type: object
      required: [check_in, check_out, occupancy]
      properties:
        check_in: { type: string, format: date, example: '2026-07-10' }
        check_out: { type: string, format: date, example: '2026-07-17' }
        occupancy: { $ref: '#/components/schemas/Occupancy' }
        currency: { type: string, description: Informational; the product prices in its own currency. }

    AvailabilityResponse:
      type: object
      properties:
        product_id: { type: string }
        type: { $ref: '#/components/schemas/PropertyType' }
        currency: { type: string, example: EUR }
        nights: { type: integer, example: 7 }
        options:
          type: array
          items: { $ref: '#/components/schemas/Option' }

    SearchRequest:
      type: object
      required: [check_in, check_out, occupancy]
      properties:
        country: { type: string, description: 'Country name or ISO 3166-1 alpha-2 code (e.g. Turkey or TR). Comma-separated for several.' }
        city: { type: string }
        type: { $ref: '#/components/schemas/PropertyType' }
        check_in: { type: string, format: date }
        check_out: { type: string, format: date }
        occupancy: { $ref: '#/components/schemas/Occupancy' }
        page: { type: integer, default: 1 }
        page_size: { type: integer, default: 20 }

    SearchResultItem:
      type: object
      properties:
        product: { $ref: '#/components/schemas/ProductSummary' }
        rate_reference: { type: string }
        price: { $ref: '#/components/schemas/Price' }

    CarCollectionType:
      type: string
      description: How the traveller collects the vehicle.
      enum: [desk_and_vehicle, meet_and_greet_vehicle, shuttle]

    CarTransmission:
      type: string
      enum: [manual, automatic]

    CarFuelType:
      type: string
      enum: [petrol, diesel, hybrid, electric, unspecified]

    CarFuelPolicy:
      type: string
      description: full_full (collect and return full), same_to_same, quarter_to_empty, full_empty, prepaid.
      enum: [full_full, same_to_same, quarter_to_empty, full_empty, prepaid]

    CarMileagePolicy:
      type: string
      enum: [unlimited, limited]

    CarChargeBasis:
      type: string
      enum: [per_day, per_rental, per_unit_per_day, per_unit_per_rental, pay_locally]

    CarExtraType:
      type: string
      enum:
        - child_seat
        - booster_seat
        - additional_driver
        - sat_nav
        - winter_tyres
        - snow_chains
        - ski_rack
        - toll_transponder
        - one_way_fee
        - out_of_hours
        - young_driver_surcharge
        - cross_border
        - delivery
        - other

    CarStation:
      type: object
      description: A pickup station. One pickup station per car product today.
      properties:
        id: { type: string }
        name: { type: string, example: Faro Airport }
        collection_type: { $ref: '#/components/schemas/CarCollectionType' }
        address: { type: string }
        city: { type: string, example: Faro }
        country: { type: string, example: Portugal }
        country_code: { type: string, nullable: true, example: PT, description: 'ISO 3166-1 alpha-2, derived from the country. Null when unrecognised.' }
        airport_code: { type: string, example: FAO }
        location: { $ref: '#/components/schemas/Location' }
        opening_hours: { type: object, additionalProperties: { type: string }, description: Day-keyed opening hours, free-form. }

    Vehicle:
      type: object
      description: A bookable vehicle group. The example_model is illustrative; the actual car is this group or similar.
      properties:
        id: { type: string }
        category: { type: string, example: Compact }
        example_model: { type: string, example: Volkswagen Polo or similar }
        acriss_code: { type: string, nullable: true, example: CDMR }
        transmission: { $ref: '#/components/schemas/CarTransmission' }
        fuel_type: { $ref: '#/components/schemas/CarFuelType' }
        seats: { type: integer, example: 5 }
        doors: { type: integer, example: 5 }
        large_bags: { type: integer, example: 1 }
        small_bags: { type: integer, example: 2 }
        air_con: { type: boolean, example: true }
        min_driver_age: { type: integer, example: 21 }
        fuel_policy: { $ref: '#/components/schemas/CarFuelPolicy' }
        mileage_policy: { $ref: '#/components/schemas/CarMileagePolicy' }
        mileage_limit: { type: integer, nullable: true, description: Present when mileage_policy is limited., example: 200 }
        mileage_limit_basis: { type: string, nullable: true, example: per_day }
        excess:
          description: Damage and theft excess for this vehicle, if it overrides the product excess.
          oneOf:
            - { $ref: '#/components/schemas/Money' }
            - { type: 'null' }
        features:
          type: array
          items: { type: string }
        images:
          type: array
          items: { type: string, format: uri }

    CarExtra:
      type: object
      description: An optional add-on. Prepaid extras are paid to you and included in the booking total; pay-local extras are surfaced for information and settled at the desk.
      properties:
        id: { type: string }
        type: { $ref: '#/components/schemas/CarExtraType' }
        name: { type: string, example: Child seat }
        charge_basis: { $ref: '#/components/schemas/CarChargeBasis' }
        price: { $ref: '#/components/schemas/Money' }
        max_qty: { type: integer, nullable: true, example: 3 }
        payable: { type: string, enum: [prepaid, local], example: prepaid }
        notes: { type: string }

    AgePolicy:
      type: object
      description: The driver age rules for the product. Surfaced from the contract age policy.
      properties:
        min_age: { type: integer, example: 21 }
        max_age: { type: integer, nullable: true, example: 75 }
        young_driver_age: { type: integer, nullable: true, description: Drivers below this age may attract a young-driver surcharge., example: 25 }
        notes: { type: string }

    CarProductSummary:
      type: object
      properties:
        id: { type: string, example: 7c2a... }
        type: { type: string, enum: [car_hire], example: car_hire }
        supplier_name: { type: string, example: Algarve Auto }
        country: { type: string, example: Portugal }
        country_code: { type: string, nullable: true, example: PT, description: 'ISO 3166-1 alpha-2, derived from the country. Null when unrecognised.' }
        region: { type: string, nullable: true, example: Algarve }
        city: { type: string, example: Faro }
        currency: { type: string, example: EUR }
        station: { $ref: '#/components/schemas/CarStation' }
        hero_image: { type: string, format: uri }
        vehicle_count: { type: integer, example: 4 }
        distance_km: { type: number, nullable: true, description: Distance from the search point in km, present only when near is used. }

    CarProduct:
      allOf:
        - { $ref: '#/components/schemas/CarProductSummary' }
        - type: object
          properties:
            station: { $ref: '#/components/schemas/CarStation' }
            age_policy: { $ref: '#/components/schemas/AgePolicy' }
            security_deposit:
              oneOf:
                - { $ref: '#/components/schemas/SecurityDeposit' }
                - { type: 'null' }
            excess:
              description: Default damage and theft excess for the product.
              oneOf:
                - { $ref: '#/components/schemas/Money' }
                - { type: 'null' }
            cover_note: { type: string, description: What insurance cover is included. }
            cancellation_notes: { type: string }
            cancellation_policy:
              type: array
              items: { $ref: '#/components/schemas/CancellationBand' }
            licence_notes: { type: string }
            cross_border_notes: { type: string }
            vehicles:
              type: array
              items: { $ref: '#/components/schemas/Vehicle' }
            extras:
              type: array
              items: { $ref: '#/components/schemas/CarExtra' }

    CarOption:
      type: object
      description: A priced, bookable vehicle result for the requested pickup, dropoff and driver age.
      properties:
        rate_reference: { type: string, description: Carry this into POST /bookings/quote }
        vehicle: { $ref: '#/components/schemas/Vehicle' }
        price: { $ref: '#/components/schemas/Price' }
        breakdown:
          type: array
          items: { $ref: '#/components/schemas/BreakdownLine' }
        fuel_policy: { $ref: '#/components/schemas/CarFuelPolicy' }
        mileage_policy: { $ref: '#/components/schemas/CarMileagePolicy' }
        mileage_limit: { type: integer, nullable: true }
        excess:
          oneOf:
            - { $ref: '#/components/schemas/Money' }
            - { type: 'null' }
        security_deposit:
          oneOf:
            - { $ref: '#/components/schemas/SecurityDeposit' }
            - { type: 'null' }
        cover_note: { type: string }
        extras:
          type: array
          items: { $ref: '#/components/schemas/CarExtra' }
        cancellation_policy:
          type: array
          items: { $ref: '#/components/schemas/CancellationBand' }
        available: { type: boolean, example: true }

    CarAvailabilityRequest:
      type: object
      required: [pickup_at, dropoff_at, driver_age]
      properties:
        pickup_at: { type: string, format: date-time, example: '2026-07-10T10:00:00Z' }
        dropoff_at: { type: string, format: date-time, example: '2026-07-17T10:00:00Z' }
        driver_age: { type: integer, example: 35 }
        driver_licence_country: { type: string, description: ISO 3166-1 alpha-2 of the issuing country, optional., example: GB }
        pickup_station_id: { type: string, description: Optional; defaults to the product pickup station. }
        dropoff_station_id: { type: string, description: Optional; defaults to the pickup station. }

    CarAvailabilityResponse:
      type: object
      properties:
        car_id: { type: string }
        type: { type: string, enum: [car_hire], example: car_hire }
        currency: { type: string, example: EUR }
        pickup_at: { type: string, format: date-time }
        dropoff_at: { type: string, format: date-time }
        rental_days: { type: integer, example: 7 }
        options:
          type: array
          items: { $ref: '#/components/schemas/CarOption' }

    CarSearchRequest:
      type: object
      required: [pickup_at, dropoff_at, driver_age]
      properties:
        pickup_at: { type: string, format: date-time }
        dropoff_at: { type: string, format: date-time }
        driver_age: { type: integer }
        ids: { type: array, items: { type: string }, description: A basket of car product ids. }
        country: { type: array, items: { type: string }, description: 'Country names or ISO 3166-1 alpha-2 codes (e.g. Turkey or TR).' }
        region: { type: array, items: { type: string } }
        city: { type: array, items: { type: string } }
        airport_code: { type: array, items: { type: string }, description: IATA codes of the pickup station airport. }
        category: { type: array, items: { type: string }, description: Vehicle category, label or ACRISS code. }
        transmission: { type: array, items: { type: string, enum: [manual, automatic] } }
        fuel_type: { type: array, items: { type: string } }
        seats_min: { type: integer }
        air_con: { type: boolean }
        supplier: { type: array, items: { type: string }, description: Supplier name contains. }
        near: { $ref: '#/components/schemas/NearFilter' }
        q: { type: string }
        sort: { type: string, enum: [name, distance] }
        page: { type: integer, default: 1 }
        page_size: { type: integer, default: 20 }

    CarSearchResultItem:
      description: 'One car product as a one-call search result: the shared availability-search envelope (product_id, price_from, option_count, lead_option) plus the whole priced fleet (options) and the complete product detail (product), alongside the original car_product/vehicle/rate_reference/price fields, kept for backward compatibility within v1.'
      type: object
      properties:
        product_id: { type: string }
        type: { type: string, enum: [car_hire] }
        supplier_name: { type: string, nullable: true }
        currency: { type: string }
        price_from: { $ref: '#/components/schemas/Price' }
        option_count: { type: integer }
        distance_km: { type: number, nullable: true, description: Present when near is used. }
        lead_option:
          type: object
          properties:
            rate_reference: { type: string }
            vehicle: { $ref: '#/components/schemas/Vehicle' }
            price: { $ref: '#/components/schemas/Price' }
        options:
          type: array
          description: The whole priced fleet for the searched dates, cheapest first, each ready for /bookings/quote.
          items: { $ref: '#/components/schemas/CarOption' }
        product:
          description: The full product detail, exactly as GET /v1/cars/{car_id} returns it.
          allOf:
            - { $ref: '#/components/schemas/CarProduct' }
        car_product: { $ref: '#/components/schemas/CarProductSummary' }
        vehicle: { $ref: '#/components/schemas/Vehicle' }
        rate_reference: { type: string }
        price: { $ref: '#/components/schemas/Price' }

    NearFilter:
      description: 'A map radius. Send an object, or the string "lat,lon,radius_km". radius_km defaults to 25.'
      type: object
      required: [lat, lon]
      properties:
        lat: { type: number, example: 37.09 }
        lon: { type: number, example: -8.25 }
        radius_km: { type: number, default: 25, example: 40 }

    PropertySearchRequest:
      description: 'Catalogue filters for accommodation. Every field is optional. Multi-value fields accept an array or a comma-separated string.'
      type: object
      properties:
        ids: { type: array, items: { type: string }, description: A basket of property ids. }
        country: { type: array, items: { type: string }, description: 'Country names or ISO 3166-1 alpha-2 codes (e.g. Turkey or TR).' }
        region: { type: array, items: { type: string }, description: Region or area; also matches the resort. }
        city: { type: array, items: { type: string }, description: City or town. }
        resort: { type: array, items: { type: string } }
        airport_code: { type: array, items: { type: string }, description: IATA codes of the nearest airport. }
        property_type: { type: array, items: { $ref: '#/components/schemas/PropertyType' } }
        board_basis: { type: array, items: { $ref: '#/components/schemas/BoardBasis' }, description: Properties offering any of these boards. }
        amenities: { type: array, items: { type: string }, description: Must-have amenities; all must be present. }
        star_min: { type: number }
        star_max: { type: number }
        near: { $ref: '#/components/schemas/NearFilter' }
        q: { type: string }
        sort: { type: string, enum: [name, distance, rating] }
        page: { type: integer, minimum: 1, default: 1 }
        page_size: { type: integer, minimum: 1, maximum: 100, default: 20 }

    TransferPlaceFilter:
      description: 'One end of a point-to-point transfer search — a SPECIFIC place, not an area. A route endpoint must satisfy every field given; a near circle is matched tightly (capped at ~5km) so it means "this place", and airports match exactly by airport_code. Used as origin and destination — a route qualifies when one endpoint matches origin and the other destination (unordered, since a route runs both ways). For a broad area search, use the top-level near/location filters instead.'
      type: object
      properties:
        near: { $ref: '#/components/schemas/NearFilter' }
        country: { type: array, items: { type: string }, description: 'Country names or ISO 3166-1 alpha-2 codes.' }
        region: { type: array, items: { type: string } }
        airport_code: { type: array, items: { type: string } }
        point_type: { type: array, items: { type: string, enum: [airport, city, resort] } }

    TransferSearchRequest:
      description: 'Catalogue filters for transfers. Every field is optional. Multi-value fields accept an array or a comma-separated string.'
      type: object
      properties:
        ids: { type: array, items: { type: string } }
        country: { type: array, items: { type: string }, description: 'Country names or ISO 3166-1 alpha-2 codes (e.g. Turkey or TR).' }
        region: { type: array, items: { type: string } }
        airport_code: { type: array, items: { type: string } }
        point_type: { type: array, items: { type: string, enum: [airport, city, resort] } }
        transfer_mode: { type: array, items: { type: string, enum: [private, shared] } }
        vehicle_class: { type: array, items: { type: string } }
        pax: { type: integer, description: Minimum passenger capacity needed. }
        meet_and_greet: { type: boolean }
        supplier: { type: array, items: { type: string } }
        near: { $ref: '#/components/schemas/NearFilter' }
        origin: { allOf: [ { $ref: '#/components/schemas/TransferPlaceFilter' } ], description: 'Point-to-point: the pickup end. Aliases from, pickup, pick_up.' }
        destination: { allOf: [ { $ref: '#/components/schemas/TransferPlaceFilter' } ], description: 'Point-to-point: the dropoff end. Aliases to, dropoff, drop_off, dest.' }
        q: { type: string }
        sort: { type: string, enum: [name, distance] }
        page: { type: integer, minimum: 1, default: 1 }
        page_size: { type: integer, minimum: 1, maximum: 100, default: 20 }

    PropertyAvailabilitySearchRequest:
      description: 'Bulk priced accommodation availability: the catalogue filters plus the stay. Dates and occupancy are required; at least one scoping filter (ids, near or a location filter) must be present.'
      allOf:
        - { $ref: '#/components/schemas/PropertySearchRequest' }
        - type: object
          required: [check_in, check_out, occupancy]
          properties:
            check_in: { type: string, format: date }
            check_out: { type: string, format: date }
            occupancy: { $ref: '#/components/schemas/Occupancy' }

    TransferAvailabilitySearchRequest:
      description: 'Bulk priced transfer availability: the catalogue filters plus the journey. date and pax are required; at least one scoping filter must be present (ids, near, a location field, or an origin/destination pair). route_id is not accepted — the cheapest qualifying route per product is priced.'
      allOf:
        - { $ref: '#/components/schemas/TransferSearchRequest' }
        - type: object
          required: [date, pax]
          properties:
            date: { type: string, format: date }
            pax: { type: integer, minimum: 1 }
            direction: { type: string, enum: [arrival, departure, any] }
            is_return: { type: boolean }
            return_date: { type: string, format: date, description: 'Return leg date; implies is_return.' }

    AccommodationAvailabilityResult:
      description: One property with at least one bookable option, and its lead (cheapest) option.
      type: object
      properties:
        product_id: { type: string }
        type: { type: string, enum: [accommodation] }
        property_type: { $ref: '#/components/schemas/PropertyType' }
        name: { type: string }
        star_rating: { type: integer, nullable: true }
        currency: { type: string }
        nights: { type: integer }
        location: { $ref: '#/components/schemas/AccommodationLocation' }
        hero_image: { $ref: '#/components/schemas/Image' }
        price_from: { $ref: '#/components/schemas/Price' }
        option_count: { type: integer, description: Bookable options the per-property availability call would return. }
        lead_option:
          type: object
          properties:
            rate_reference: { type: string }
            room_id: { type: string }
            room_name: { type: string }
            board_basis: { $ref: '#/components/schemas/BoardBasis' }
            price: { $ref: '#/components/schemas/Price' }
            available_units: { type: integer }
            cancellation_policy:
              type: array
              items: { $ref: '#/components/schemas/CancellationBand' }
            security_deposit:
              oneOf:
                - { $ref: '#/components/schemas/Money' }
                - { type: 'null' }
            stay_rules:
              type: array
              items:
                type: object
                properties:
                  min_nights: { type: integer }
                  max_nights: { type: integer }
                  arrival_days: { type: array, items: { type: string } }
        options:
          type: array
          description: 'Every bookable room and rate for this property, not just the lead. Each carries its own rate_reference (feeds quote/hold/confirm) and cancellation policy. Options that fail a stay rule (minimum or maximum nights, or arrival day) for the searched dates, or whose room cannot take the occupancy sent (max_adults, max_children, max_occupancy), are not returned.'
          items:
            type: object
            properties:
              rate_reference: { type: string }
              room_id: { type: string }
              room_name: { type: string }
              board_basis: { $ref: '#/components/schemas/BoardBasis' }
              price: { $ref: '#/components/schemas/Price' }
              price_per_night: { $ref: '#/components/schemas/Money' }
              available_units: { type: integer }
              cancellation_policy:
                type: array
                items: { $ref: '#/components/schemas/CancellationBand' }
        product:
          description: The full content GET /v1/properties/{property_id} would return, so a search result can render the detail page without a second call.
          allOf:
            - { $ref: '#/components/schemas/Accommodation' }

    TransferAvailabilityResult:
      description: 'One transfer product with at least one qualifying option. A one-call search result: the lead (cheapest) option for a grid, plus the full priced options, the bookable extras and the complete product detail, so no per-product detail or availability call is needed.'
      type: object
      properties:
        product_id: { type: string }
        type: { type: string, enum: [transfer] }
        supplier_name: { type: string, nullable: true }
        currency: { type: string }
        price_from: { $ref: '#/components/schemas/Money' }
        distance_km: { type: number, nullable: true, description: Present when near is used. }
        option_count: { type: integer }
        lead_option:
          type: object
          properties:
            rate_reference: { type: string }
            route:
              type: object
              properties:
                id: { type: string }
                from: { type: string, nullable: true, description: 'Where the journey starts. When the search named both an origin and a destination, this is the end that matched the origin, so the journey reads the way it was searched rather than the way the route happens to be stored.' }
                to: { type: string, nullable: true, description: The other end, where the journey finishes. }
            vehicle:
              type: object
              properties:
                class: { type: string, nullable: true }
                pricing_basis: { type: string, enum: [per_vehicle, per_person] }
                max_pax: { type: integer, nullable: true }
            direction: { type: string, enum: [arrival, departure, any], description: 'Resolved from the search when an origin and a destination were both given: arrival runs airport to resort, departure runs resort to airport. Stays "any" when the search did not say which way round.' }
            legs: { type: integer }
            is_return: { type: boolean }
            journey:
              type: array
              description: The trip leg by leg, starting from the origin that was searched.
              items: { $ref: '#/components/schemas/TransferJourneyLeg' }
            price: { $ref: '#/components/schemas/Money' }
        options:
          type: array
          description: Every priced route-and-vehicle combination for the searched journey, each ready for /bookings/quote.
          items: { $ref: '#/components/schemas/TransferOption' }
        extras:
          type: array
          description: The product's bookable add-ons, priced.
          items: { $ref: '#/components/schemas/TransferExtra' }
        product:
          description: The full product detail, exactly as GET /v1/transfers/{transfer_id} returns it.
          allOf:
            - { $ref: '#/components/schemas/Transfer' }

    QuoteRequest:
      type: object
      required: [rate_reference]
      properties:
        rate_reference: { type: string }
        tickets:
          description: 'Attraction tickets only: how many of each ticket type to price. Ignored by other products.'
          type: array
          items:
            type: object
            required: [ticket_type_id, qty]
            properties:
              ticket_type_id: { type: string }
              qty: { type: integer, minimum: 1 }
        travellers:
          description: 'Tours, coaches and trains: how many of each traveller/passenger type to price for the departure. Ignored by other products. (Coaches also accept the same list as "passengers".)'
          type: array
          items:
            type: object
            required: [ticket_type_id, qty]
            properties:
              ticket_type_id: { type: string }
              qty: { type: integer, minimum: 1 }
        passengers:
          description: >
            Two shapes, by product. Flights: an object, the passenger mix to
            price. Coaches and trains: an array of the passenger types being
            bought, which is how the guide's examples send it. Ignored by the
            other products.
          oneOf:
            - type: object
              properties:
                adults: { type: integer, minimum: 1 }
                children: { type: integer, minimum: 0 }
                infants: { type: integer, minimum: 0 }
            - type: array
              items:
                type: object
                properties:
                  passenger_type_id: { type: string, format: uuid }
                  qty: { type: integer, minimum: 1 }
        sailing_id:
          type: string
          description: >-
            Coaches only, optional, and the right way to quote a cruise round
            trip. The sailing names its own return coach, so a run feeding two
            cruises books the correct one for each. Takes precedence over
            return_date. From any departure's sailings[].sailing_id. It must
            belong to the departure this rate_reference already pins, otherwise
            the quote is refused.
        return_date:
          type: string
          format: date
          description: >-
            Coaches only, optional. Add it to quote a return trip: the paired
            inbound coach (same pick-up town, that date) is priced too and the
            quote total becomes the sum of both one-way fares. At hold the seats
            are reduced on both legs, and the booking carries a return_leg. Omit
            for a one-way, exactly as before. The route must offer a return.
            Prefer sailing_id on a cruise feeder: a date alone cannot tell two
            cruises on the same coach apart.

    Quote:
      type: object
      properties:
        quote_id: { type: string }
        expires_at: { type: string, format: date-time }
        product:
          type: object
          properties:
            id: { type: string }
            type: { $ref: '#/components/schemas/BookableType' }
            name: { type: string }
        room:
          description: accommodation only
          oneOf:
            - { $ref: '#/components/schemas/RoomRef' }
            - { type: 'null' }
        board_basis: { $ref: '#/components/schemas/BoardBasis' }
        occupancy: { $ref: '#/components/schemas/Occupancy' }
        check_in: { type: string, format: date, description: accommodation only }
        check_out: { type: string, format: date, description: accommodation only }
        nights: { type: integer, description: accommodation and transfer (legs for transfer carries elsewhere) }
        route:
          description: 'transfer only. The route as it was at the moment of booking. Unlike a SEARCH result, this is not reoriented: point_a and point_b are in the order the route is stored, and the snapshot is frozen so renaming a point later never changes an existing booking. Read `direction` for which way the journey runs.'
          oneOf:
            - { $ref: '#/components/schemas/TransferRoute' }
            - { type: 'null' }
        direction: { type: string, description: 'transfer only. Which way the journey runs: arrival is airport to resort, departure is resort to airport, any when the booking was made without saying.', enum: [arrival, departure, any] }
        pax: { type: integer, description: transfer only }
        legs: { type: integer, description: 'transfer only: 1 one way, 2 return' }
        is_return: { type: boolean, description: 'transfers and coaches: true when a return leg was priced into this quote.' }
        return_date: { type: string, format: date, nullable: true, description: 'transfers and coaches: the return leg date, when one was priced. For a coach the breakdown also carries a "Return coach" line and the outbound lines are marked "(outbound)".' }
        pricing_basis: { type: string, description: transfer only, enum: [per_vehicle, per_person] }
        vehicle:
          description: 'car hire (a Vehicle) or transfer (a TransferVehicle)'
          oneOf:
            - { $ref: '#/components/schemas/Vehicle' }
            - { $ref: '#/components/schemas/TransferVehicle' }
            - { type: 'null' }
        pickup_at: { type: string, format: date-time, description: car hire only }
        dropoff_at: { type: string, format: date-time, description: car hire only }
        rental_days: { type: integer, description: car hire only }
        fuel_policy: { $ref: '#/components/schemas/CarFuelPolicy' }
        mileage_policy: { $ref: '#/components/schemas/CarMileagePolicy' }
        excess:
          description: car hire only
          oneOf:
            - { $ref: '#/components/schemas/Money' }
            - { type: 'null' }
        price: { $ref: '#/components/schemas/Price' }
        breakdown:
          type: array
          items: { $ref: '#/components/schemas/BreakdownLine' }
        security_deposit:
          oneOf:
            - { $ref: '#/components/schemas/SecurityDeposit' }
            - { type: 'null' }
        extras:
          type: array
          description: car hire only; the prepaid extras priced into this quote.
          items: { $ref: '#/components/schemas/CarExtra' }
        option:
          description: attraction only
          oneOf:
            - { $ref: '#/components/schemas/TicketOptionDef' }
            - { type: 'null' }
        visit_date: { type: string, format: date, nullable: true, description: attraction only }
        valid_until: { type: string, format: date, nullable: true, description: 'attraction only; for a duration ticket bought against a date.' }
        tickets:
          type: array
          description: attraction only; the priced ticket lines.
          items:
            type: object
            properties:
              ticket_type_id: { type: string }
              name: { type: string }
              quantity: { type: integer }
              price_per_ticket: { $ref: '#/components/schemas/Money' }
              price: { $ref: '#/components/schemas/Money' }
        trip_type: { type: string, enum: [one_way, return], description: flights only }
        cabin: { type: string, enum: [Y, W, J, F], description: 'flights only: Economy, Premium Economy, Business, First' }
        cabin_name: { type: string, description: flights only }
        departure_date:
          description: 'coaches and trains: the date the quoted service runs.'
          type: string
          format: date
          nullable: true
        pick_up:
          description: 'coaches only: the boarding point quoted, with its board time and position. Trains return boarding_station instead.'
          oneOf:
            - { $ref: '#/components/schemas/CoachPickUpPoint' }
            - { type: 'null' }
        boarding_station:
          description: 'trains only: the station quoted, with its departure time and position.'
          oneOf:
            - { $ref: '#/components/schemas/TrainStationPoint' }
            - { type: 'null' }
        destination:
          description: 'coaches and trains: the other end of the route.'
          type: string
          nullable: true
        passengers:
          description: >
            Two shapes, by product. Flights: an object, the quoted passenger mix.
            Coaches and trains: an array of the quoted passenger types with their
            per-seat and line prices.
          oneOf:
            - type: object
              properties:
                adults: { type: integer }
                children: { type: integer }
                infants: { type: integer }
            - type: array
              items:
                type: object
                properties:
                  passenger_type_id: { type: string, format: uuid }
                  name: { type: string, example: Adult }
                  quantity: { type: integer, example: 2 }
                  price_per_seat: { $ref: '#/components/schemas/Money' }
                  price: { $ref: '#/components/schemas/Money' }
        outbound:
          description: flights only; the outbound direction (segments and per-passenger fares).
          oneOf:
            - { $ref: '#/components/schemas/FlightDirection' }
            - { type: 'null' }
        return:
          description: flights only; the return direction, when a return was quoted.
          oneOf:
            - { $ref: '#/components/schemas/FlightDirection' }
            - { type: 'null' }
        cancellation_policy:
          type: array
          items: { $ref: '#/components/schemas/CancellationBand' }

    LeadGuest:
      type: object
      required: [first_name, last_name, email]
      properties:
        title: { type: string, example: Ms }
        first_name: { type: string }
        last_name: { type: string }
        email: { type: string, format: email }
        phone: { type: string }

    Guest:
      type: object
      required: [type, first_name, last_name]
      properties:
        type: { type: string, enum: [adult, child, infant] }
        first_name: { type: string }
        last_name: { type: string }
        date_of_birth: { type: string, format: date }
        age: { type: integer }

    Driver:
      type: object
      description: The lead driver for a car booking. Held server-side only and never returned in full.
      required: [first_name, last_name]
      properties:
        title: { type: string, example: Mr }
        first_name: { type: string }
        last_name: { type: string }
        date_of_birth: { type: string, format: date }
        age: { type: integer, example: 35 }
        licence_number: { type: string }
        licence_country: { type: string, description: ISO 3166-1 alpha-2 of the issuing country., example: GB }

    SelectedExtra:
      type: object
      description: A prepaid extra the traveller is buying. Pay-local extras are not sent here.
      required: [extra_id]
      properties:
        extra_id: { type: string }
        quantity: { type: integer, default: 1, example: 1 }

    HoldRequest:
      type: object
      required: [quote_id, lead_guest]
      properties:
        quote_id: { type: string }
        external_ref: { type: string, description: Your own reference. The engine treats (source, external_ref) as unique. }
        lead_guest: { $ref: '#/components/schemas/LeadGuest' }
        guests:
          type: array
          description: 'accommodation and transfers. For a transfer: the optional named passenger list (type adult, child or infant, defaulting to adult, plus first_name, last_name and optionally age; max 50). It is stored on the booking and printed on the supplier letter as the passenger manifest with the adult, child and infant split.'
          items: { $ref: '#/components/schemas/Guest' }
        driver:
          description: car hire only; required for a car booking.
          oneOf:
            - { $ref: '#/components/schemas/Driver' }
            - { type: 'null' }
        extras:
          type: array
          description: 'car hire, transfers, attractions and flights: prepaid extras to add to the booking, priced against the contract and added to the total. Mandatory extras are already in the quoted price and pay-local extras cannot be prepaid; sending either is a 422, as is an attraction extra scoped to a different option.'
          items: { $ref: '#/components/schemas/SelectedExtra' }
        additional_drivers:
          type: array
          description: car hire only; named additional drivers.
          items:
            type: object
            properties:
              first_name: { type: string }
              last_name: { type: string }
        seats:
          type: array
          description: >
            Optional seat selection for flights, coaches and trains. For flights each
            seat needs a leg_id; for coaches and trains the seat code alone is enough
            (the seat type and any surcharge are resolved from the service seat plan).
            Get the plan from POST /flights|coaches|trains/{id}/seatmap. Any surcharge
            is added to the booking total.
          items:
            type: object
            required: [seat]
            properties:
              passenger_ref: { type: [string, integer, 'null'] }
              leg_id: { type: string, description: flights only }
              seat: { type: string, example: 12A }
        transfer_details:
          type: object
          description: 'transfers only: extra journey details, passed on to the supplier booking letter. All optional. Also accepted under the key journey_details.'
          properties:
            flight_number: { type: string, example: BA346 }
            flight_arrival_time: { type: string, description: 'For an arrival transfer (airport to resort), e.g. "9 Jan 2027, 14:25".' }
            flight_departure_time: { type: string, description: 'The departing flight''s time: the inbound (return) leg on a return, or the outbound flight on a departure transfer.' }
            return_flight_number: { type: string, example: BA347, description: 'The inbound (return) flight number, shown in the letter''s Inbound block.' }
            child_seats: { type: string, example: '1 child seat, 1 booster', description: 'Child or booster seat needs, shown as Child seat requirements in the letter''s Service section.' }
            extra_luggage: { type: string, example: '2 ski bags', description: 'Bags beyond the vehicle''s standard allowance, shown as Additional luggage in the letter''s Service section.' }
            accommodation_name: { type: string, example: 'Hotel Pavillon, Courmayeur' }
            agent_email: { type: string, format: email, example: sarah@agency.example, description: 'The account manager handling this booking. They are copied on the supplier letter and set as its reply-to, so they hold the letter that went out and receive the supplier''s reply. Must be a valid email address. When omitted the reply-to falls back to the account-wide address and nobody extra is copied.' }
            questions:
              type: array
              maxItems: 50
              description: 'Free-form question/answer pairs collected at booking, stored against the booking and shown to the contract owner in submitted order. Optional. No dedup on name; order is preserved. Omit or send an empty array for none.'
              items:
                type: object
                required: [name, answer]
                properties:
                  name: { type: string, maxLength: 200, description: 'Non-empty. e.g. "Booster seats".' }
                  answer: { type: string, maxLength: 1000, description: 'Non-empty. e.g. "2".' }
        notes: { type: string }

    ConfirmRequest:
      type: object
      required: [payment_reference]
      properties:
        payment_reference: { type: string }
        payment_status: { type: string, enum: [paid, deposit_paid] }
        amount_paid: { $ref: '#/components/schemas/Money' }

    AmendRequest:
      type: object
      properties:
        lead_guest: { $ref: '#/components/schemas/LeadGuest' }
        guests:
          type: array
          description: accommodation only
          items: { $ref: '#/components/schemas/Guest' }
        driver:
          description: car hire only
          oneOf:
            - { $ref: '#/components/schemas/Driver' }
            - { type: 'null' }
        notes: { type: string }

    CancelRequest:
      type: object
      properties:
        reason: { type: string }

    Booking:
      type: object
      properties:
        id: { type: string, example: bkg_01J8... }
        reference: { type: string, example: TGC-7F3K9Q }
        status: { $ref: '#/components/schemas/BookingStatus' }
        voucher_url: { type: string, format: uri, description: 'A signed, shareable link to the branded voucher PDF. Hand it to the traveller, or fetch the PDF programmatically at GET /bookings/{booking_id}/voucher.' }
        confirmation_message: { type: string, description: 'The agent''s own booking confirmation message for this product type, set on the Brand page. White-label, suitable to show or email to the traveller. Present only when one is configured and the booking is live (not on cancelled or expired bookings).' }
        product:
          type: object
          properties:
            id: { type: string }
            type: { $ref: '#/components/schemas/BookableType' }
            name: { type: string }
        room:
          description: accommodation only
          oneOf:
            - { $ref: '#/components/schemas/RoomRef' }
            - { type: 'null' }
        board_basis: { $ref: '#/components/schemas/BoardBasis' }
        check_in: { type: string, format: date, description: accommodation only }
        check_out: { type: string, format: date, description: accommodation only }
        nights: { type: integer, description: accommodation only }
        occupancy: { $ref: '#/components/schemas/Occupancy' }
        route:
          description: transfer only
          oneOf:
            - { $ref: '#/components/schemas/TransferRoute' }
            - { type: 'null' }
        direction: { type: string, description: transfer only, enum: [arrival, departure, any] }
        pax: { type: integer, description: transfer only }
        legs: { type: integer, description: transfer only }
        is_return: { type: boolean, description: 'transfers and coaches: true when this booking includes a return leg. For a coach the seats were reduced on both departures and a return_leg is present.' }
        travel_date: { type: string, format: date, description: transfer only }
        transfer_details:
          type: object
          description: 'transfer only; the extra journey details captured at hold time (flight, accommodation and any question/answer pairs). Present only when at least one was supplied. Questions are echoed in submitted order.'
          properties:
            flight_number: { type: string, example: BA346 }
            flight_arrival_time: { type: string }
            flight_departure_time: { type: string }
            return_flight_number: { type: string }
            child_seats: { type: string }
            extra_luggage: { type: string }
            accommodation_name: { type: string, example: 'Hotel Pavillon, Courmayeur' }
            agent_email: { type: string, format: email }
            questions:
              type: array
              items:
                type: object
                properties:
                  name: { type: string, example: 'Booster seats' }
                  answer: { type: string, example: '2' }
        option:
          description: attraction only
          oneOf:
            - type: object
              properties:
                id: { type: string }
                name: { type: string }
            - { type: 'null' }
        visit_date: { type: string, format: date, description: attraction only }
        valid_until: { type: string, format: date, description: attraction only }
        tickets:
          type: array
          description: attraction only; the booked ticket lines.
          items:
            type: object
            properties:
              ticket_type_id: { type: string }
              name: { type: string }
              quantity: { type: integer }
        trip_type: { type: string, enum: [one_way, return], description: flights only }
        cabin: { type: string, enum: [Y, W, J, F], description: flights only }
        outbound_date: { type: string, format: date, description: flights only }
        return_date: { type: string, format: date, nullable: true, description: 'flights, transfers and coaches: the return leg date, when one was booked.' }
        return_leg:
          description: 'coaches only; present when is_return is true. The booked inbound coach.'
          oneOf:
            - { $ref: '#/components/schemas/CoachReturnLeg' }
            - { type: 'null' }
        departure_date:
          description: 'coaches and trains: the date the service runs. Tours use this field too.'
          type: string
          format: date
          nullable: true
        pick_up:
          description: >
            coaches only: the boarding point booked, as it was named at the time.
            On a return-direction route this is where the traveller is set down;
            see the route's direction. Trains return boarding_station instead.
          oneOf:
            - type: object
              properties:
                id: { type: string, format: uuid }
                name: { type: string }
            - { type: 'null' }
        boarding_station:
          description: 'trains only: the station booked, as it was named at the time.'
          oneOf:
            - type: object
              properties:
                id: { type: string, format: uuid }
                name: { type: string }
            - { type: 'null' }
        destination:
          description: 'coaches and trains: the other end of the route, from the booking-time snapshot.'
          type: string
          nullable: true
        passengers:
          description: >
            Two shapes, by product. Flights: an object of passenger counts (names
            are stored but never returned). Coaches and trains: an array of the
            booked passenger types with their quantities and prices, which is
            what the booking is actually priced from.
          oneOf:
            - type: object
              properties:
                adults: { type: integer }
                children: { type: integer }
                infants: { type: integer }
            - type: array
              items:
                type: object
                properties:
                  ticket_type_id: { type: string, format: uuid }
                  name: { type: string, example: Adult }
                  quantity: { type: integer, example: 2 }
                  price: { $ref: '#/components/schemas/Money' }
                  line_total: { type: number, example: 70.00 }
        itinerary:
          description: flights only; the booked itinerary snapshot (outbound and return directions).
          oneOf:
            - type: object
              properties:
                outbound: { $ref: '#/components/schemas/FlightDirection' }
                return:
                  oneOf:
                    - { $ref: '#/components/schemas/FlightDirection' }
                    - { type: 'null' }
            - { type: 'null' }
        seats:
          type: array
          description: 'The chosen seats (flights, coaches and trains; no passenger PII). leg_id and cabin are flights only; seat_type and seat_type_name are coaches and trains only.'
          items:
            type: object
            properties:
              passenger_ref: { type: [string, integer, 'null'] }
              leg_id: { type: string, description: flights only }
              seat: { type: string, example: 12A }
              cabin: { type: string, nullable: true, description: flights only }
              seat_type: { type: string, nullable: true, description: 'coaches and trains only; the seat type (zone) code.' }
              seat_type_name: { type: string, nullable: true, description: 'coaches and trains only.' }
        vehicle:
          description: 'car hire (a Vehicle) or transfer (a TransferVehicle)'
          oneOf:
            - { $ref: '#/components/schemas/Vehicle' }
            - { $ref: '#/components/schemas/TransferVehicle' }
            - { type: 'null' }
        pickup_at: { type: string, format: date-time, description: car hire only }
        dropoff_at: { type: string, format: date-time, description: car hire only }
        rental_days: { type: integer, description: car hire only }
        pickup_station: { $ref: '#/components/schemas/CarStation' }
        dropoff_station: { $ref: '#/components/schemas/CarStation' }
        fuel_policy: { $ref: '#/components/schemas/CarFuelPolicy' }
        mileage_policy: { $ref: '#/components/schemas/CarMileagePolicy' }
        excess:
          description: car hire only
          oneOf:
            - { $ref: '#/components/schemas/Money' }
            - { type: 'null' }
        extras:
          type: array
          description: car hire only; the prepaid extras on this booking.
          items: { $ref: '#/components/schemas/CarExtra' }
        lead_guest:
          type: object
          description: For a car booking this is the lead booker; the driver is held separately and not returned in full.
          properties:
            name: { type: string }
            email: { type: string }
        price: { $ref: '#/components/schemas/Price' }
        security_deposit:
          oneOf:
            - { $ref: '#/components/schemas/SecurityDeposit' }
            - { type: 'null' }
        cancellation_policy:
          type: array
          items: { $ref: '#/components/schemas/CancellationBand' }
        source: { type: string, example: your-account }
        external_ref: { type: string, example: YOUR-REF-10482 }
        expires_at: { type: string, format: date-time, description: present while status is held }
        hold_window_seconds: { type: integer, description: present while status is held }
        created_at: { type: string, format: date-time }
        confirmed_at: { type: string, format: date-time }

    CoachReturnLeg:
      type: object
      description: 'The inbound leg of a round-trip coach: its date, boarding pick-up and route. The pick-up town is the same as the outbound; there are no separate return times.'
      properties:
        departure_date: { type: string, format: date, example: '2027-01-20' }
        pick_up:
          type: object
          properties:
            id: { type: string, nullable: true }
            name: { type: string, nullable: true, example: Cardiff }
        route:
          description: The inbound route (the "from Southampton" direction). Null if not recorded.
          oneOf:
            - type: object
              properties:
                id: { type: string, nullable: true }
                name: { type: string, example: 'Cardiff cruise coach from Southampton (2027)' }
            - { type: 'null' }

    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              enum: [invalid_request, unauthenticated, forbidden, not_found, availability_changed, unprocessable, rate_limited, server_error]
            message: { type: string }
            request_id: { type: string, example: req_8f2c1a }

    # ---- Shared small shapes ----
    RoomRef:
      type: object
      description: 'The room as it was at the moment of booking. Snapshotted, so editing the room later never changes a booking that already exists, and a booking made before a field existed simply does not carry it.'
      properties:
        id: { type: string }
        name: { type: string, example: Double Room }
        bedding: { type: string, example: '1 double bed or 2 singles', description: Present when the room records it. }
        max_occupancy: { type: integer, description: Present when the room records it. }
        size_sqm: { type: number, description: Present when the room records it. }

    Image:
      type: object
      properties:
        url: { type: string, format: uri }
        caption: { type: string }

    # ---- Accommodation ----
    AccommodationLocation:
      type: object
      properties:
        country: { type: string, example: Portugal }
        country_code: { type: string, nullable: true, example: PT, description: 'ISO 3166-1 alpha-2, derived from the country. Null when unrecognised.' }
        region: { type: string, nullable: true, example: Algarve }
        city: { type: string, example: Albufeira }
        resort: { type: string, nullable: true }
        address: { type: string, nullable: true }
        coordinates:
          oneOf:
            - type: object
              properties:
                latitude: { type: number, example: 37.0891 }
                longitude: { type: number, example: -8.2475 }
            - { type: 'null' }
        nearest_airport:
          oneOf:
            - type: object
              properties:
                code: { type: string, example: FAO }
                name: { type: string, nullable: true, example: Faro }
                distance_km: { type: number, nullable: true, example: 38 }
            - { type: 'null' }

    AccommodationSummary:
      type: object
      properties:
        id: { type: string }
        type: { type: string, enum: [accommodation], example: accommodation }
        name: { type: string, example: Vale do Sol Beach Hotel }
        property_type: { $ref: '#/components/schemas/PropertyType' }
        star_rating: { type: integer, nullable: true, example: 4 }
        currency: { type: string, nullable: true, example: EUR }
        location: { $ref: '#/components/schemas/AccommodationLocation' }
        room_count: { type: integer, example: 5 }
        board_basis:
          description: The board bases this property offers.
          type: array
          items: { $ref: '#/components/schemas/BoardBasis' }
        distance_km: { type: number, nullable: true, description: Distance from the search point in km, present only when near is used. }
        hero_image: { $ref: '#/components/schemas/Image' }

    AccommodationRoom:
      type: object
      properties:
        id: { type: string }
        name: { type: string, example: Twin Garden View }
        description: { type: string, nullable: true }
        max_occupancy:
          type: object
          properties:
            total: { type: integer, nullable: true, example: 3 }
            adults: { type: integer, nullable: true, example: 2 }
            children: { type: integer, nullable: true, example: 1 }
            infants: { type: integer, nullable: true, example: 1 }
        bedrooms: { type: integer, nullable: true }
        bathrooms: { type: number, nullable: true }
        size_sqm: { type: number, nullable: true, example: 24 }
        floor: { type: string, nullable: true }
        smoking: { type: boolean }
        beds:
          description: The bedding configuration as loaded, or null.
          oneOf:
            - { type: object }
            - { type: array, items: {} }
            - { type: 'null' }
        amenities:
          type: array
          items: { type: string }
        images:
          type: array
          items: { $ref: '#/components/schemas/Image' }

    Accommodation:
      allOf:
        - { $ref: '#/components/schemas/AccommodationSummary' }
        - type: object
          properties:
            description: { type: string, nullable: true }
            amenities:
              type: array
              items: { type: string }
            check_in_time: { type: string, nullable: true, example: '15:00' }
            check_out_time: { type: string, nullable: true, example: '11:00' }
            policies:
              type: object
              properties:
                min_checkin_age: { type: integer, nullable: true, example: 18 }
                tourist_tax:
                  oneOf:
                    - type: object
                      properties:
                        amount: { type: number, nullable: true }
                        currency: { type: string, nullable: true }
                        basis: { type: string, nullable: true }
                        payable: { type: string, example: at_property }
                    - { type: 'null' }
                notes: { type: string, nullable: true }
            rooms:
              type: array
              items: { $ref: '#/components/schemas/AccommodationRoom' }
            images:
              type: array
              items: { $ref: '#/components/schemas/Image' }

    AccommodationOption:
      type: object
      description: A priced, bookable room result for the requested dates and occupancy.
      properties:
        rate_reference: { type: string, description: Carry this into POST /bookings/quote }
        room: { $ref: '#/components/schemas/RoomRef' }
        board_basis: { $ref: '#/components/schemas/BoardBasis' }
        occupancy: { $ref: '#/components/schemas/Occupancy' }
        nights: { type: integer, example: 7 }
        price: { $ref: '#/components/schemas/Price' }
        price_per_night:
          oneOf:
            - { $ref: '#/components/schemas/Money' }
            - { type: 'null' }
        breakdown:
          type: array
          items: { $ref: '#/components/schemas/BreakdownLine' }
        available_units: { type: integer, description: 'Units of this room type free for the whole stay, net of in-app reservations, closures and live API holds.', example: 3 }
        cancellation_policy:
          type: array
          items: { $ref: '#/components/schemas/CancellationBand' }
        available: { type: boolean, example: true }

    AccommodationAvailabilityRequest:
      type: object
      required: [check_in, check_out]
      description: >
        Ask in one of three ways, and only one per request. `occupancy` prices a
        single room. `rooms` prices several rooms where you say who is in each.
        `party` prices several rooms where we work out what fits. Sending more
        than one is refused rather than guessed at.
      properties:
        check_in: { type: string, format: date, example: '2026-07-10' }
        check_out: { type: string, format: date, example: '2026-07-17' }
        occupancy:
          allOf: [{ $ref: '#/components/schemas/Occupancy' }]
          description: One room. Unchanged, and still the way most callers ask.
        rooms:
          type: array
          minItems: 1
          maxItems: 9
          description: >
            One entry per room wanted, each with its own guests. Every room needs
            at least one adult. Children are ages, never a count, because the age
            is what sets the price.
          items: { $ref: '#/components/schemas/Occupancy' }
          example: [{ adults: 2, children: [6] }, { adults: 2, children: [] }]
        party:
          allOf: [{ $ref: '#/components/schemas/Occupancy' }]
          description: >
            The whole group, with no room split. The reply suggests the workable
            room combinations, cheapest first, each one bookable as it stands.
        max_rooms:
          type: integer
          minimum: 1
          maximum: 9
          description: Only with `party`. Caps how many rooms a suggestion may use.
        board_basis: { $ref: '#/components/schemas/BoardBasis' }
        room_id: { type: string, description: Optional; price a single room type. }

    AccommodationAvailabilityResponse:
      type: object
      description: >
        With `occupancy` each option is one room. With `rooms` or `party` each
        option is a whole basket of rooms with one price and one rate_reference,
        and carries a `rooms` list instead of a single room.
      properties:
        property_id: { type: string }
        check_in: { type: string, format: date }
        check_out: { type: string, format: date }
        nights: { type: integer, example: 7 }
        occupancy:
          allOf: [{ $ref: '#/components/schemas/Occupancy' }]
          description: Present when you asked with `occupancy`.
        rooms_requested:
          type: array
          description: Present when you asked with `rooms`.
          items: { $ref: '#/components/schemas/Occupancy' }
        party:
          allOf: [{ $ref: '#/components/schemas/Occupancy' }]
          description: Present when you asked with `party`.
        currency: { type: string, nullable: true, example: EUR }
        options:
          type: array
          items:
            oneOf:
              - { $ref: '#/components/schemas/AccommodationOption' }
              - { $ref: '#/components/schemas/AccommodationBasket' }

    AccommodationBasket:
      type: object
      description: >
        Several rooms priced as one. Quote and hold it with its rate_reference
        exactly as you would a single room. Holding is all or nothing: if any
        room has gone the whole basket is refused, so a group is never part
        booked.
      properties:
        rate_reference:
          type: string
          description: Pass to POST /bookings/quote. Starts `accb_`.
          example: accb_eyJ2IjoxLCJrIjoiYWNjb21tb2RhdGlvbl9iYXNrZXQi...
        board_basis: { $ref: '#/components/schemas/BoardBasis' }
        rooms_count: { type: integer, example: 2 }
        nights: { type: integer, example: 7 }
        guests:
          type: object
          description: Everyone in the basket, across all its rooms.
          properties:
            adults: { type: integer, example: 4 }
            children: { type: array, items: { type: integer }, example: [6] }
        price:
          allOf: [{ $ref: '#/components/schemas/Price' }]
          description: The whole basket. The sum of its rooms.
        rooms:
          type: array
          description: One entry per room, priced on its own guests.
          items:
            type: object
            properties:
              room: { $ref: '#/components/schemas/RoomRef' }
              occupancy: { $ref: '#/components/schemas/Occupancy' }
              price: { $ref: '#/components/schemas/Price' }
              breakdown:
                type: array
                items: { $ref: '#/components/schemas/BreakdownLine' }
              cancellation_policy:
                type: array
                items: { $ref: '#/components/schemas/CancellationBand' }
        cancellation_policy:
          type: array
          description: >
            The basket's terms. Where the rooms differ this states the strictest
            charge at each notice, because that is what the booker is held to.
          items: { $ref: '#/components/schemas/CancellationBand' }
        available: { type: boolean, example: true }

    # ---- Transfers ----
    TransferPoint:
      type: object
      properties:
        id: { type: string }
        name: { type: string, example: Courmayeur, description: 'The name to show travellers: the operator''s own display name when one is set, otherwise the full geolocated name. Use this everywhere you present the point.' }
        full_name: { type: string, example: 'Courmayeur City Center, Courmayeur, Valle d''Aosta, Italy', description: 'Always the full geolocated place name, unchanged by any display name.' }
        type: { type: string, enum: [airport, city, resort], example: airport }
        airport_code: { type: string, nullable: true, example: FAO }
        region: { type: string, nullable: true }
        country: { type: string, nullable: true, example: Portugal }
        country_code: { type: string, nullable: true, example: PT, description: 'ISO 3166-1 alpha-2, derived from the country. Null when unrecognised.' }
        location:
          oneOf:
            - type: object
              properties:
                latitude: { type: number }
                longitude: { type: number }
            - { type: 'null' }

    TransferRoute:
      type: object
      properties:
        id: { type: string }
        label: { type: string, example: 'Courmayeur ↔ Geneva Airport (GVA)', description: 'Built from the two points'' resolved names (display name when set).' }
        point_a: { $ref: '#/components/schemas/TransferPoint' }
        point_b: { $ref: '#/components/schemas/TransferPoint' }
        approx_distance_km: { type: number, nullable: true }
        approx_duration_min: { type: number, nullable: true }
        vehicle_ids:
          type: array
          items: { type: string }
          description: 'Product detail only: the vehicles with at least one rate on this route, i.e. which vehicles can ever serve it. The availability call remains the truth for a specific date, direction and party.'

    TransferVehicle:
      type: object
      properties:
        id: { type: string }
        label: { type: string, example: Saloon car }
        mode: { type: string, enum: [private, shared], example: private }
        vehicle_class: { type: string, nullable: true, example: saloon }
        min_pax: { type: integer, nullable: true, example: 1 }
        max_pax: { type: integer, nullable: true, example: 4 }
        luggage_allowance: { type: integer, nullable: true, example: 3, description: 'Bags allowed PER PERSON, not per vehicle. Render as "N bags per person".' }
        luggage_notes: { type: string, nullable: true }
        features:
          type: array
          items: { type: string }
        images:
          type: array
          items: { $ref: '#/components/schemas/Image' }

    TransferExtra:
      type: object
      properties:
        id: { type: string }
        type: { type: string }
        name: { type: string }
        charge_basis: { type: string }
        price:
          oneOf:
            - { $ref: '#/components/schemas/Money' }
            - { type: 'null' }
        max_qty: { type: integer, nullable: true }
        payable: { type: string, nullable: true }
        notes: { type: string }

    TransferSummary:
      type: object
      properties:
        id: { type: string }
        type: { type: string, enum: [transfer], example: transfer }
        supplier_name: { type: string, nullable: true }
        currency: { type: string, example: EUR }
        countries:
          type: array
          items: { type: string }
        country_codes:
          type: array
          items: { type: string }
          description: 'ISO 3166-1 alpha-2 codes matching countries. Unrecognised names are omitted.'
        regions:
          type: array
          items: { type: string }
        route_count: { type: integer, example: 4 }
        vehicle_count: { type: integer, example: 3 }
        sample_route: { type: string, nullable: true, example: Faro Airport (FAO) ↔ Albufeira, description: 'A representative route label, built from the points'' resolved names (display name when set).' }
        meet_and_greet: { type: boolean }
        booking_cutoff_hours: { type: integer, nullable: true, example: 24 }
        distance_km: { type: number, nullable: true, description: Distance from the search point in km, present only when near is used. }
        hero_image: { $ref: '#/components/schemas/Image' }

    Transfer:
      type: object
      properties:
        id: { type: string }
        type: { type: string, enum: [transfer], example: transfer }
        supplier_name: { type: string, nullable: true }
        currency: { type: string, example: EUR }
        meet_and_greet: { type: boolean }
        booking_cutoff_hours: { type: integer, nullable: true }
        cancellation_notes: { type: string }
        terms_notes: { type: string }
        child_policy:
          description: The contract's child policy object, or null.
          oneOf:
            - { type: object }
            - { type: 'null' }
        points:
          type: array
          items: { $ref: '#/components/schemas/TransferPoint' }
        routes:
          type: array
          items: { $ref: '#/components/schemas/TransferRoute' }
        vehicles:
          type: array
          items: { $ref: '#/components/schemas/TransferVehicle' }
        extras:
          type: array
          items: { $ref: '#/components/schemas/TransferExtra' }

    TransferJourneyLeg:
      type: object
      description: One leg of the trip as it is actually travelled.
      properties:
        leg: { type: integer, description: '1 for the outbound, 2 for the return.' }
        type: { type: string, enum: [one_way, outbound, return] }
        date: { type: string, format: date, nullable: true }
        from: { type: string, nullable: true, description: Where this leg starts. }
        to: { type: string, nullable: true, description: Where this leg finishes. }
        direction: { type: string, enum: [arrival, departure, any] }

    TransferOption:
      type: object
      description: A priced, bookable transfer result for the requested journey.
      properties:
        rate_reference: { type: string, description: Carry this into POST /bookings/quote }
        route:
          description: 'The route oriented to THIS option''s journey when the search gave both an origin and a destination: point_a is where the passenger is picked up on leg 1 and point_b where they are dropped off, and `label`, `from` and `to` all read the same way round. So an arrival carries the airport in point_a and a departure carries it in point_b, and the same stored route is emitted either way round depending on the search. On a return the pair stays on the outbound leg; the way home is journey[1]. Where the search did not say which way round, the stored order stands.'
          allOf:
            - { $ref: '#/components/schemas/TransferRoute' }
        vehicle: { $ref: '#/components/schemas/TransferVehicle' }
        direction: { type: string, enum: [arrival, departure, any], description: 'Resolved from the search when an origin and a destination were both given: arrival runs airport to resort, departure runs resort to airport. Stays "any" when the search did not say which way round.' }
        pax: { type: integer }
        legs: { type: integer, description: 1 one way, 2 return }
        is_return: { type: boolean }
        return_date: { type: string, format: date, nullable: true, description: Present when the return was priced with its own date. }
        pricing_basis: { type: string, enum: [per_vehicle, per_person] }
        journey:
          type: array
          description: 'The trip leg by leg, in the order travelled. A return is the outbound leg then the same journey reversed, so a round trip can be shown without inferring it from the leg count. When the search gave an origin and a destination, leg 1 starts from the origin that was searched.'
          items: { $ref: '#/components/schemas/TransferJourneyLeg' }
        price: { $ref: '#/components/schemas/Price' }
        price_per_unit:
          type: object
          properties:
            amount: { type: number }
            currency: { type: string }
            basis: { type: string, enum: [per_person, per_vehicle] }
        breakdown:
          type: array
          items: { $ref: '#/components/schemas/BreakdownLine' }
        cancellation_policy:
          type: array
          items: { $ref: '#/components/schemas/CancellationBand' }
        available: { type: boolean, example: true }

    TransferAvailabilityRequest:
      type: object
      required: [date, pax]
      properties:
        date: { type: string, format: date, example: '2026-07-10' }
        pax: { type: integer, minimum: 1, example: 3 }
        route_id: { type: string, description: Optional; price a single route. }
        direction: { type: string, enum: [arrival, departure, any], default: any }
        is_return: { type: boolean, default: false }
        return_date: { type: string, format: date, description: 'Return leg date; implies is_return. Same day as or after date. Each leg then prices on its own date and direction. Omitted: a return prices as two legs of the outbound date.' }

    TransferAvailabilityResponse:
      type: object
      properties:
        transfer_id: { type: string }
        date: { type: string, format: date }
        pax: { type: integer }
        direction: { type: string, enum: [arrival, departure, any] }
        is_return: { type: boolean }
        return_date: { type: string, format: date, nullable: true }
        currency: { type: string, nullable: true, example: EUR }
        options:
          type: array
          items: { $ref: '#/components/schemas/TransferOption' }
        extras:
          type: array
          items: { $ref: '#/components/schemas/TransferExtra' }

    TicketType:
      type: object
      properties:
        id: { type: string }
        name: { type: string, example: Adult }
        pricing_basis: { type: string, enum: [per_person, group], example: per_person }
        min_age: { type: integer, nullable: true, example: 13 }
        max_age: { type: integer, nullable: true }
        group_size: { type: integer, nullable: true, description: 'group pricing only: how many people the bundle admits.' }
        group_composition: { type: object, description: 'group pricing only: an optional roster e.g. { adult: 2, child: 2 }.' }
        proof_required: { type: boolean, description: 'Present when proof of eligibility (age, student, etc.) is required at entry.' }
        eligibility: { type: string, description: 'age, student, disability, military, ebt or resident.' }

    TicketOptionDef:
      type: object
      description: A sellable variant of an attraction (e.g. Day Ticket, 3-Day Park Hopper, 48 Hour).
      properties:
        id: { type: string }
        name: { type: string, example: 3-Day Park Hopper }
        description: { type: string }
        duration:
          description: How long admission lasts, or null for a single visit.
          oneOf:
            - type: object
              properties:
                value: { type: integer, example: 3 }
                unit: { type: string, enum: [day, hour, month], example: day }
            - { type: 'null' }
        entry_type: { type: string, enum: [single, multi_day, multi_entry, unlimited], example: multi_day }
        requires_visit_date: { type: boolean }
        validity_window_days: { type: integer, nullable: true, description: 'Use the ticket within this many days of first use.' }
        activation: { type: string, enum: [on_purchase, on_first_use] }
        timed: { type: boolean, description: 'True when the option uses time slots (a later stage).' }

    TicketExtra:
      type: object
      properties:
        id: { type: string }
        type: { type: string, nullable: true }
        name: { type: string, example: Fast track }
        charge_basis: { type: string, nullable: true }
        price:
          oneOf:
            - { $ref: '#/components/schemas/Money' }
            - { type: 'null' }
        payable: { type: string, nullable: true, enum: [prepaid, local] }

    TicketSummary:
      type: object
      properties:
        id: { type: string }
        type: { type: string, enum: [attraction, event], example: attraction }
        name: { type: string, example: Eiffel Tower }
        supplier_name: { type: string, nullable: true }
        currency: { type: string, example: EUR }
        category: { type: string, nullable: true }
        country: { type: string, nullable: true }
        country_code: { type: string, nullable: true, example: FR }
        region: { type: string, nullable: true }
        city: { type: string, nullable: true, example: Paris }
        option_count: { type: integer, example: 3 }
        ticket_type_count: { type: integer, example: 2 }
        booking_cutoff_hours: { type: integer, nullable: true }
        distance_km: { type: number, nullable: true, description: Present only when near is used. }
        hero_image: { $ref: '#/components/schemas/Image' }

    Ticket:
      type: object
      properties:
        id: { type: string }
        type: { type: string, enum: [attraction, event], example: attraction }
        name: { type: string }
        supplier_name: { type: string, nullable: true }
        currency: { type: string, example: EUR }
        category: { type: string, nullable: true }
        country: { type: string, nullable: true }
        country_code: { type: string, nullable: true }
        region: { type: string, nullable: true }
        city: { type: string, nullable: true }
        location:
          oneOf:
            - type: object
              properties:
                latitude: { type: number }
                longitude: { type: number }
            - { type: 'null' }
        description: { type: string }
        opening_times: { type: string }
        images:
          type: array
          items: { $ref: '#/components/schemas/Image' }
        valid_from: { type: string, format: date, nullable: true }
        valid_to: { type: string, format: date, nullable: true }
        booking_cutoff_hours: { type: integer, nullable: true }
        cancellation_notes: { type: string }
        terms_notes: { type: string }
        options:
          type: array
          items: { $ref: '#/components/schemas/TicketOptionDef' }
        ticket_types:
          type: array
          items: { $ref: '#/components/schemas/TicketType' }
        extras:
          type: array
          items: { $ref: '#/components/schemas/TicketExtra' }

    TicketOption:
      type: object
      description: A priced, bookable option for the requested date, with its price-per-ticket list.
      properties:
        rate_reference: { type: string, description: 'Carry this, with the chosen tickets, into POST /bookings/quote.' }
        option: { $ref: '#/components/schemas/TicketOptionDef' }
        product:
          type: object
          properties:
            id: { type: string }
            type: { type: string, enum: [attraction, event] }
            name: { type: string }
        visit_date: { type: string, format: date, nullable: true }
        valid_until: { type: string, format: date, nullable: true, description: 'For a duration ticket bought against a date.' }
        ticket_types:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              name: { type: string }
              pricing_basis: { type: string, enum: [per_person, group] }
              min_age: { type: integer, nullable: true }
              max_age: { type: integer, nullable: true }
              price_per_ticket: { $ref: '#/components/schemas/Money' }
              quantity: { type: integer, description: Present when quantities were supplied. }
              price: { $ref: '#/components/schemas/Money' }
        price_from: { $ref: '#/components/schemas/Money' }
        price:
          description: The basket total, present when quantities were supplied.
          oneOf:
            - { $ref: '#/components/schemas/Money' }
            - { type: 'null' }
        cancellation_policy:
          type: array
          items: { $ref: '#/components/schemas/CancellationBand' }
        available: { type: boolean, example: true }

    TicketAvailabilityRequest:
      type: object
      properties:
        date: { type: string, format: date, nullable: true, example: '2026-07-10', description: 'The visit or start date. Omit only for open-dated options.' }
        option_id: { type: string, description: Optional; price a single option. }
        channel: { type: string, enum: [online, gate], default: online }
        tickets:
          description: Optional; how many of each ticket type, to total a basket.
          type: array
          items:
            type: object
            required: [ticket_type_id, qty]
            properties:
              ticket_type_id: { type: string }
              qty: { type: integer, minimum: 1 }

    TicketAvailabilityResponse:
      type: object
      properties:
        ticket_id: { type: string }
        date: { type: string, format: date, nullable: true }
        currency: { type: string, nullable: true, example: EUR }
        reason: { type: string, description: 'When options is empty, why (e.g. not_on_sale, outside_validity, within_cutoff, on_stop_sale, sold_out).' }
        options:
          type: array
          items: { $ref: '#/components/schemas/TicketOption' }
        extras:
          type: array
          items: { $ref: '#/components/schemas/TicketExtra' }

    TicketSearchRequest:
      description: 'Catalogue filters for attractions. Every field is optional. Multi-value fields accept an array or a comma-separated string.'
      type: object
      properties:
        ids: { type: array, items: { type: string } }
        kind: { type: array, items: { type: string, enum: [attraction, event] } }
        country: { type: array, items: { type: string }, description: 'Country names or ISO 3166-1 alpha-2 codes (e.g. France or FR).' }
        region: { type: array, items: { type: string } }
        city: { type: array, items: { type: string } }
        category: { type: array, items: { type: string } }
        supplier: { type: array, items: { type: string } }
        near: { $ref: '#/components/schemas/NearFilter' }
        q: { type: string }
        sort: { type: string, enum: [name, distance] }
        page: { type: integer, minimum: 1, default: 1 }
        page_size: { type: integer, minimum: 1, maximum: 100, default: 20 }

    TicketAvailabilitySearchRequest:
      description: 'Bulk priced attraction availability: the catalogue filters plus an optional visit date. At least one scoping filter (ids, near, country, region, city or category) must be present. Omit the date to surface only open-dated products.'
      allOf:
        - { $ref: '#/components/schemas/TicketSearchRequest' }
        - type: object
          properties:
            date: { type: string, format: date }
            channel: { type: string, enum: [online, gate] }

    TicketAvailabilityResult:
      description: One attraction with at least one bookable option, and its lead (cheapest) option.
      type: object
      properties:
        product_id: { type: string }
        type: { type: string, enum: [attraction, event] }
        name: { type: string }
        supplier_name: { type: string, nullable: true }
        currency: { type: string }
        price_from: { $ref: '#/components/schemas/Money' }
        distance_km: { type: number, nullable: true, description: Present when near is used. }
        option_count: { type: integer }
        visit_date: { type: string, format: date, nullable: true }
        lead_option:
          type: object
          properties:
            rate_reference: { type: string }
            id: { type: string }
            name: { type: string }
            duration:
              oneOf:
                - type: object
                  properties:
                    value: { type: integer }
                    unit: { type: string, enum: [day, hour, month] }
                - { type: 'null' }
            entry_type: { type: string, enum: [single, multi_day, multi_entry, unlimited] }
            price_from: { $ref: '#/components/schemas/Money' }
        options:
          type: array
          description: Every bookable option priced, each ready for /bookings/quote.
          items: { $ref: '#/components/schemas/TicketOption' }
        extras:
          type: array
          description: The attraction's bookable add-ons, priced. Book one by sending its id in extras at hold.
          items: { $ref: '#/components/schemas/TicketExtra' }
        product:
          description: The full product detail, exactly as GET /v1/tickets/{ticket_id} returns it.
          allOf:
            - { $ref: '#/components/schemas/Ticket' }

    TourSummary:
      type: object
      properties:
        id: { type: string }
        type: { type: string, enum: [tour], example: tour }
        name: { type: string, example: Highlights of Morocco }
        supplier_name: { type: string, nullable: true }
        currency: { type: string, example: GBP }
        category: { type: string, nullable: true }
        country: { type: string, nullable: true }
        country_code: { type: string, nullable: true }
        region: { type: string, nullable: true }
        city: { type: string, nullable: true }
        duration_days: { type: integer, nullable: true, example: 8 }
        duration_nights: { type: integer, nullable: true, example: 7 }
        start_point: { type: string, nullable: true }
        end_point: { type: string, nullable: true }
        departure_count: { type: integer, example: 5 }
        upcoming_count: { type: integer, example: 4 }
        next_departure: { type: string, format: date, nullable: true }
        price_from:
          oneOf:
            - { $ref: '#/components/schemas/Money' }
            - { type: 'null' }
        booking_cutoff_hours: { type: integer, nullable: true }
        distance_km: { type: number, nullable: true, description: Present only when near is used. }
        hero_image: { $ref: '#/components/schemas/Image' }

    Departure:
      type: object
      description: One dated run of a tour - the unit of availability and inventory.
      properties:
        id: { type: string }
        departure_date: { type: string, format: date }
        return_date: { type: string, format: date, nullable: true }
        status: { type: string, enum: [available, guaranteed, on_request, closed, cancelled] }
        season_id: { type: string, nullable: true }
        capacity: { type: integer, nullable: true, description: 'null means unlimited.' }
        spaces_left: { type: integer, nullable: true, description: 'capacity minus booked; null when uncapped.' }
        min_to_run: { type: integer, nullable: true }
        bookable: { type: boolean }
        price_from:
          oneOf:
            - { $ref: '#/components/schemas/Money' }
            - { type: 'null' }

    Tour:
      type: object
      properties:
        id: { type: string }
        type: { type: string, enum: [tour] }
        name: { type: string }
        supplier_name: { type: string, nullable: true }
        currency: { type: string }
        category: { type: string, nullable: true }
        country: { type: string, nullable: true }
        country_code: { type: string, nullable: true }
        region: { type: string, nullable: true }
        city: { type: string, nullable: true }
        location:
          oneOf:
            - type: object
              properties:
                latitude: { type: number }
                longitude: { type: number }
            - { type: 'null' }
        duration_days: { type: integer, nullable: true }
        duration_nights: { type: integer, nullable: true }
        start_point: { type: string, nullable: true }
        end_point: { type: string, nullable: true }
        description: { type: string }
        images:
          type: array
          items: { $ref: '#/components/schemas/Image' }
        min_age: { type: integer, nullable: true }
        group_size:
          type: object
          properties:
            min: { type: integer, nullable: true }
            max: { type: integer, nullable: true }
        inclusions: { type: string }
        exclusions: { type: string }
        itinerary:
          type: array
          items:
            type: object
            properties:
              day: { type: integer }
              title: { type: string, nullable: true }
              detail: { type: string, nullable: true }
        deposit:
          oneOf:
            - type: object
              properties:
                type: { type: string, enum: [percent, fixed] }
                value: { type: number }
            - { type: 'null' }
        balance_due_days: { type: integer, nullable: true }
        cancellation_policy:
          type: array
          items:
            type: object
            properties:
              days_before: { type: integer }
              charge_pct: { type: number }
        cancellation_notes: { type: string }
        booking_cutoff_hours: { type: integer, nullable: true }
        valid_from: { type: string, format: date, nullable: true }
        valid_to: { type: string, format: date, nullable: true }
        room_bases:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              name: { type: string }
        traveller_types:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              name: { type: string }
              min_age: { type: integer, nullable: true }
              max_age: { type: integer, nullable: true }
        seasons:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              name: { type: string, nullable: true }
              start_date: { type: string, format: date }
              end_date: { type: string, format: date }
        extras:
          type: array
          items: { $ref: '#/components/schemas/TicketExtra' }
        departures:
          type: array
          items: { $ref: '#/components/schemas/Departure' }

    TourRoomBasis:
      type: object
      description: A priced, bookable room basis for a departure, with its per-person prices.
      properties:
        rate_reference: { type: string, description: 'Carry this, with the chosen travellers, into POST /bookings/quote.' }
        room_basis:
          type: object
          properties:
            id: { type: string }
            name: { type: string }
        traveller_types:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              name: { type: string }
              min_age: { type: integer, nullable: true }
              max_age: { type: integer, nullable: true }
              price_per_person: { $ref: '#/components/schemas/Money' }
              quantity: { type: integer, description: Present when quantities were supplied. }
              price: { $ref: '#/components/schemas/Money' }
        price_from: { $ref: '#/components/schemas/Money' }
        price:
          description: The basket total, present when quantities were supplied.
          oneOf:
            - { $ref: '#/components/schemas/Money' }
            - { type: 'null' }
        cancellation_policy:
          type: array
          items:
            type: object
            properties:
              days_before: { type: integer }
              charge_pct: { type: number }
        available: { type: boolean }

    TourAvailabilityRequest:
      type: object
      description: Pin the departure by departure_id or departure_date. travellers is optional (omit to see per-person prices only).
      properties:
        departure_id: { type: string }
        departure_date: { type: string, format: date, example: '2026-07-04' }
        travellers:
          type: array
          items:
            type: object
            required: [ticket_type_id, qty]
            properties:
              ticket_type_id: { type: string }
              qty: { type: integer, minimum: 1 }

    TourAvailabilityResponse:
      type: object
      properties:
        tour_id: { type: string }
        departure: { $ref: '#/components/schemas/Departure' }
        currency: { type: string, nullable: true }
        reason: { type: string, description: 'When room_bases is empty, why (e.g. not_on_sale, within_cutoff, on_request, on_stop_sale, sold_out, insufficient_spaces, departed). insufficient_spaces means the travellers you sent exceed the spaces left.' }
        room_bases:
          type: array
          items: { $ref: '#/components/schemas/TourRoomBasis' }
        extras:
          type: array
          items: { $ref: '#/components/schemas/TicketExtra' }

    TourSearchRequest:
      description: 'Catalogue filters for tours. Every field is optional. Multi-value fields accept an array or a comma-separated string.'
      type: object
      properties:
        ids: { type: array, items: { type: string } }
        country: { type: array, items: { type: string } }
        region: { type: array, items: { type: string } }
        city: { type: array, items: { type: string } }
        category: { type: array, items: { type: string } }
        supplier: { type: array, items: { type: string } }
        near: { $ref: '#/components/schemas/NearFilter' }
        q: { type: string }
        sort: { type: string, enum: [name, distance] }
        page: { type: integer, minimum: 1, default: 1 }
        page_size: { type: integer, minimum: 1, maximum: 100, default: 20 }

    TourAvailabilitySearchRequest:
      description: A scoping filter (ids, near, country, region, city or category) is required.
      allOf:
        - { $ref: '#/components/schemas/TourSearchRequest' }
        - type: object
          properties:
            date_from: { type: string, format: date, description: Start of the departure window. Defaults to today. }
            date_to: { type: string, format: date, description: End of the departure window. Omit for no upper bound. }
            travellers:
              description: Optional traveller basket. Departures without enough spaces for it are excluded, and each room basis carries a basket price.
              type: array
              items:
                type: object
                required: [ticket_type_id, qty]
                properties:
                  ticket_type_id: { type: string }
                  qty: { type: integer, minimum: 1 }

    TourAvailabilityResult:
      description: 'One tour with at least one bookable departure in the window. A one-call search result: the priced departures, the lead (cheapest) room basis, the extras and the complete product detail, so no per-tour detail or availability call is needed.'
      type: object
      properties:
        product_id: { type: string }
        type: { type: string, enum: [tour] }
        name: { type: string }
        supplier_name: { type: string, nullable: true }
        currency: { type: string }
        price_from: { $ref: '#/components/schemas/Money' }
        distance_km: { type: number, nullable: true, description: Present when near is used. }
        hero_image: { $ref: '#/components/schemas/Image' }
        departure_count: { type: integer, description: 'All bookable departures in the window, not just the listed ones.' }
        lead_option:
          description: The cheapest priced room basis across the listed departures.
          type: object
          properties:
            rate_reference: { type: string }
            departure_id: { type: string }
            departure_date: { type: string, format: date }
            room_basis:
              type: object
              properties:
                id: { type: string }
                name: { type: string }
            price_from: { $ref: '#/components/schemas/Money' }
        departures:
          type: array
          description: 'Up to 10 bookable departures in the window, soonest first, each fully priced.'
          items:
            type: object
            properties:
              departure: { $ref: '#/components/schemas/Departure' }
              room_bases:
                type: array
                items: { $ref: '#/components/schemas/TourRoomBasis' }
              price_from:
                oneOf:
                  - { $ref: '#/components/schemas/Money' }
                  - { type: 'null' }
        extras:
          type: array
          description: The tour's bookable add-ons, priced.
          items: { $ref: '#/components/schemas/TicketExtra' }
        product:
          description: The full product detail, exactly as GET /v1/tours/{tour_id} returns it.
          allOf:
            - { $ref: '#/components/schemas/Tour' }

    CoachSummary:
      type: object
      properties:
        id: { type: string }
        type: { type: string, enum: [coach], example: coach }
        name: { type: string, example: South Wales to Southampton Cruise }
        operator: { type: string, nullable: true }
        vehicle: { type: string, nullable: true, example: Coach }
        direction: { type: string, enum: [outbound, return], example: outbound }
        currency: { type: string, example: GBP }
        country: { type: string, nullable: true }
        country_code: { type: string, nullable: true }
        region: { type: string, nullable: true }
        city: { type: string, nullable: true }
        terminal: { type: string, nullable: true, description: The single port or airport. }
        start_point: { type: string, nullable: true, description: Origin (the terminal on a return). }
        destination: { type: string, nullable: true }
        round_trip:
          type: boolean
          description: >
            True when this route is paired with a return route, so a round trip
            can be quoted by sending return_date. Check this before offering a
            return: without a pair the quote replies unprocessable.
        return_route_id:
          type: string
          format: uuid
          nullable: true
          description: The paired return route, when round_trip is true.
        location:
          description: Coordinates of the terminal (sourced via Locate stops).
          oneOf:
            - type: object
              properties: { latitude: { type: number }, longitude: { type: number } }
            - { type: 'null' }
        origin_location:
          description: 'Coordinates of the origin, resolved by direction: the pick-up towns (their first located town) outbound, the terminal on a return.'
          oneOf:
            - type: object
              properties: { latitude: { type: number }, longitude: { type: number } }
            - { type: 'null' }
        destination_location:
          description: 'Coordinates of the destination, resolved by direction: the terminal outbound (e.g. the airport), the pick-up towns (first located town) on a return. Use this to place the drop-off; every town is on pick_up_points.'
          oneOf:
            - type: object
              properties: { latitude: { type: number }, longitude: { type: number } }
            - { type: 'null' }
        pick_up_count: { type: integer, example: 4 }
        departure_count: { type: integer, example: 12 }
        upcoming_count: { type: integer, example: 8 }
        next_departure: { type: string, format: date, nullable: true }
        price_from:
          oneOf:
            - { $ref: '#/components/schemas/Money' }
            - { type: 'null' }
        booking_cutoff_hours: { type: integer, nullable: true }
        distance_km: { type: number, nullable: true }
        pick_up_points:
          type: array
          description: 'Only when requested with include=pick_up_points (or include=all). The full pick-up points, as GET /coaches/{coach_id} returns them.'
          items:
            type: object
            properties:
              id: { type: string }
              name: { type: string }
              board_time: { type: string, nullable: true, example: '06:30' }
              latitude: { type: number, nullable: true }
              longitude: { type: number, nullable: true }
        passenger_types:
          type: array
          description: 'Only when requested with include=passenger_types (or include=all).'
          items:
            type: object
            properties:
              id: { type: string }
              name: { type: string }
              min_age: { type: integer, nullable: true }
              max_age: { type: integer, nullable: true }
        seasons:
          type: array
          description: 'Only when requested with include=seasons (or include=all).'
          items:
            type: object
            properties:
              id: { type: string }
              name: { type: string, nullable: true }
              start_date: { type: string, format: date }
              end_date: { type: string, format: date }
        departures:
          type: array
          description: 'Only when requested with include=departures (or include=all). Every dated run, each carrying its sailings.'
          items: { $ref: '#/components/schemas/CoachDeparture' }

    CoachDeparture:
      type: object
      description: One dated run of a coach route - the unit of availability and inventory.
      properties:
        id: { type: string }
        departure_date: { type: string, format: date }
        sailings:
          type: array
          description: >
            Present only on coaches that feed a scheduled service (for example a
            cruise-port transfer): the sailing(s) this run connects to, so a search
            box can offer the boat/sailing first, then its date(s). A run can serve
            more than one sailing (two cruises leaving on the same day). Omitted on a
            plain point-to-point coach.
          items:
            type: object
            properties:
              sailing_id:
                type: string
                description: >
                  Opaque id for THIS cruise on THIS coach. Send it to
                  POST /coaches/availability to get that cruise's two legs
                  priced, and to POST /bookings/quote as sailing_id to add the
                  matching inbound leg. A run feeding two cruises gives each its
                  own id, so the right return coach is chosen for each. No expiry.
                example: sail_eyJ2IjoxLCJrIjoic2FpbCJ9
              name: { type: string, description: 'The sailing or cruise name.', example: Norwegian Fjords }
              ship: { type: string, description: 'The vessel, when known.', example: Balmoral }
              reference: { type: string, description: 'The operator''s voyage or service reference, when known.', example: L2724 }
              return_date:
                type: string
                format: date
                description: >
                  On an outbound sailing, the date of the paired inbound coach, so
                  a round-trip search box can offer the return without the caller
                  working it out. Absent when the run names no pair.
                example: '2027-05-09'
        status: { type: string, enum: [available, guaranteed, on_request, closed, cancelled] }
        season_id: { type: string, nullable: true }
        capacity: { type: integer, nullable: true, description: 'Seats; null means unlimited.' }
        seats_left: { type: integer, nullable: true }
        min_to_run: { type: integer, nullable: true }
        bookable: { type: boolean }
        price_from:
          oneOf:
            - { $ref: '#/components/schemas/Money' }
            - { type: 'null' }

    Coach:
      type: object
      properties:
        id: { type: string }
        type: { type: string, enum: [coach] }
        name: { type: string }
        operator: { type: string, nullable: true }
        vehicle: { type: string, nullable: true }
        direction: { type: string, enum: [outbound, return] }
        currency: { type: string }
        country: { type: string, nullable: true }
        country_code: { type: string, nullable: true }
        region: { type: string, nullable: true }
        city: { type: string, nullable: true }
        location:
          description: Coordinates of the terminal.
          oneOf:
            - type: object
              properties:
                latitude: { type: number }
                longitude: { type: number }
            - { type: 'null' }
        origin_location:
          description: 'Coordinates of the origin, resolved by direction: the pick-up towns (first located town) outbound, the terminal on a return. Every town is on pick_up_points below.'
          oneOf:
            - type: object
              properties: { latitude: { type: number }, longitude: { type: number } }
            - { type: 'null' }
        destination_location:
          description: 'Coordinates of the destination, resolved by direction: the terminal outbound (e.g. the airport), the pick-up towns (first located town) on a return. Use this to place the drop-off; every town is on pick_up_points below.'
          oneOf:
            - type: object
              properties: { latitude: { type: number }, longitude: { type: number } }
            - { type: 'null' }
        terminal: { type: string, nullable: true }
        start_point: { type: string, nullable: true }
        destination: { type: string, nullable: true }
        description: { type: string }
        images:
          type: array
          items: { $ref: '#/components/schemas/Image' }
        booking_cutoff_hours: { type: integer, nullable: true }
        valid_from: { type: string, format: date, nullable: true }
        valid_to: { type: string, format: date, nullable: true }
        pick_up_points:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              name: { type: string }
              board_time: { type: string, nullable: true, example: '06:30' }
              latitude: { type: number, nullable: true }
              longitude: { type: number, nullable: true }
        passenger_types:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              name: { type: string }
              min_age: { type: integer, nullable: true }
              max_age: { type: integer, nullable: true }
        seasons:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              name: { type: string, nullable: true }
              start_date: { type: string, format: date }
              end_date: { type: string, format: date }
        cancellation_policy:
          type: array
          description: The route's cancellation bands, worst notice last. The same bands are stamped on a booking made from it.
          items: { $ref: '#/components/schemas/CancellationBand' }
        cancellation_notes: { type: string, nullable: true, description: Free text terms shown alongside the bands. }
        round_trip:
          type: boolean
          description: >
            True when this route is paired with a return route, so a round trip
            can be quoted by sending return_date. Check this before offering a
            return: without a pair the quote replies unprocessable.
        return_route_id:
          type: string
          format: uuid
          nullable: true
          description: The paired return route, when round_trip is true.
        departures:
          type: array
          items: { $ref: '#/components/schemas/CoachDeparture' }

    CoachPickUpPoint:
      type: object
      description: A pick-up point as it is named on a route, with its board time and position.
      properties:
        id: { type: string, format: uuid }
        name: { type: string, example: Cardiff Gate Services (M4 J30) }
        board_time: { type: string, nullable: true, description: 'Local time to be at the stop, HH:MM.', example: '09:50' }
        latitude: { type: number, nullable: true }
        longitude: { type: number, nullable: true }

    TrainStationPoint:
      type: object
      description: A station as it is named on a service, with its departure time and position.
      properties:
        id: { type: string, format: uuid }
        name: { type: string, example: Cardiff Central }
        board_time: { type: string, nullable: true, description: 'Local departure time from this station, HH:MM.', example: '07:30' }
        latitude: { type: number, nullable: true }
        longitude: { type: number, nullable: true }

    CoachPickUp:
      type: object
      description: A priced, bookable pick-up point for a departure, with its per-seat prices.
      properties:
        rate_reference: { type: string, description: 'Carry this, with the chosen passengers, into POST /bookings/quote.' }
        pick_up:
          type: object
          properties:
            id: { type: string }
            name: { type: string }
            board_time: { type: string, nullable: true }
            latitude: { type: number, nullable: true }
            longitude: { type: number, nullable: true }
        passenger_types:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              name: { type: string }
              min_age: { type: integer, nullable: true }
              max_age: { type: integer, nullable: true }
              price_per_seat: { $ref: '#/components/schemas/Money' }
              quantity: { type: integer, description: Present when quantities were supplied. }
              price: { $ref: '#/components/schemas/Money' }
        price_from: { $ref: '#/components/schemas/Money' }
        price:
          description: The basket total, present when quantities were supplied.
          oneOf:
            - { $ref: '#/components/schemas/Money' }
            - { type: 'null' }
        available: { type: boolean }

    CoachAvailabilityRequest:
      type: object
      description: Pin the departure by departure_id or departure_date. passengers is optional (omit to see per-seat prices only).
      properties:
        departure_id: { type: string }
        departure_date: { type: string, format: date, example: '2026-07-04' }
        passengers:
          type: array
          items:
            type: object
            required: [passenger_type_id, qty]
            properties:
              passenger_type_id: { type: string }
              qty: { type: integer, minimum: 1 }

    CoachAvailabilityResponse:
      type: object
      properties:
        coach_id: { type: string }
        departure: { $ref: '#/components/schemas/CoachDeparture' }
        currency: { type: string, nullable: true }
        reason: { type: string, description: 'When pick_up_points is empty, why (e.g. not_on_sale, within_cutoff, on_request, on_stop_sale, cancelled, sold_out, insufficient_seats, departed, no_boarding_point_available). insufficient_seats means the passengers you sent exceed the seats left, and no_boarding_point_available that every boarding point dropped out on its own (a passenger type with no fare there, a closure naming that stop, or the vehicle having already passed it). departed covers a run that has already pulled away, judged by the LAST boarding point on the route; a stop the coach has already passed, or one inside the booking cut-off, simply drops out of pick_up_points.' }
        pick_up_points:
          type: array
          items: { $ref: '#/components/schemas/CoachPickUp' }

    CoachAvailabilitySearchRequest:
      type: object
      description: >
        Search bookable coach departures across all routes by geography, date and
        party, or price ONE cruise by sending sailing_id. A date is required
        unless sailing_id is sent.
      properties:
        date: { type: string, format: date, example: '2026-07-04', description: 'Required unless sailing_id is sent.' }
        sailing_id:
          type: string
          description: >
            One cruise. The answer is exactly two results, that sailing's
            outbound coach and its inbound coach, each priced separately and
            labelled with leg. Nothing else is returned, and no date is needed
            because the sailing carries its own. From any departure's
            sailings[].sailing_id.
          example: sail_eyJ2IjoxLCJrIjoic2FpbCJ9
        sailing:
          type: string
          description: >
            Narrow an ordinary date search to one cruise. Matched on the sailing
            name, the ship or the voyage reference. Ignored when sailing_id is
            sent. The alias ship is accepted.
          example: The Crowning Jewels
        pick_up:
          description: Where the passenger boards (a town for an outbound route, the terminal for a return).
          type: object
          properties:
            lat: { type: number }
            lon: { type: number }
            radius_km: { type: number, default: 25 }
        destination:
          description: Where the passenger alights (the terminal for an outbound route, a town for a return).
          type: object
          properties:
            lat: { type: number }
            lon: { type: number }
            radius_km: { type: number, default: 25 }
        passengers:
          type: object
          properties:
            adults: { type: integer, minimum: 0 }
            children: { type: integer, minimum: 0 }
            infants: { type: integer, minimum: 0 }
        page: { type: integer, minimum: 1, default: 1 }
        page_size: { type: integer, minimum: 1, maximum: 100, default: 20 }

    CoachAvailabilityResult:
      type: object
      description: One matching route with its bookable, in-radius pick-up points for the date and party.
      properties:
        leg:
          type: string
          enum: [outbound, inbound]
          description: >
            Present only on a sailing_id search. "outbound" is the leg the
            sailing_id names, "inbound" is its return. Absent on a date search.
        reason:
          type: string
          description: >
            Present only on a sailing_id search, when nothing on this leg can be
            sold. A date search leaves such routes out instead; a cruise you
            asked for by id comes back with the reason rather than vanishing.
        id: { type: string }
        name: { type: string }
        direction: { type: string, enum: [outbound, return] }
        operator: { type: string, nullable: true }
        vehicle: { type: string, nullable: true }
        currency: { type: string }
        terminal: { type: string, nullable: true }
        location:
          description: Coordinates of the terminal.
          oneOf:
            - type: object
              properties: { latitude: { type: number }, longitude: { type: number } }
            - { type: 'null' }
        origin_location:
          description: 'Coordinates of the origin, resolved by direction (the pick-up towns, first located town, outbound; the terminal on a return).'
          oneOf:
            - type: object
              properties: { latitude: { type: number }, longitude: { type: number } }
            - { type: 'null' }
        destination_location:
          description: 'Coordinates of the destination, resolved by direction (the terminal outbound; the pick-up towns, first located town, on a return).'
          oneOf:
            - type: object
              properties: { latitude: { type: number }, longitude: { type: number } }
            - { type: 'null' }
        start_point: { type: string, nullable: true }
        destination: { type: string, nullable: true }
        distance_km: { type: number, nullable: true, description: Distance from the pick-up search point. }
        departure: { $ref: '#/components/schemas/CoachDeparture' }
        pick_up_points:
          type: array
          items: { $ref: '#/components/schemas/CoachPickUp' }
        price_from:
          oneOf:
            - { $ref: '#/components/schemas/Money' }
            - { type: 'null' }
        product:
          description: The full route detail, exactly as GET /v1/coaches/{coach_id} returns it.
          allOf:
            - { $ref: '#/components/schemas/Coach' }

    CoachSearchRequest:
      description: 'Catalogue filters for coach routes. Every field is optional. Multi-value fields accept an array or a comma-separated string.'
      type: object
      properties:
        ids: { type: array, items: { type: string } }
        country: { type: array, items: { type: string } }
        region: { type: array, items: { type: string } }
        city: { type: array, items: { type: string } }
        operator: { type: array, items: { type: string } }
        near: { $ref: '#/components/schemas/NearFilter' }
        q: { type: string }
        sort: { type: string, enum: [name, distance] }
        page: { type: integer, minimum: 1, default: 1 }
        page_size: { type: integer, minimum: 1, maximum: 100, default: 20 }

    TrainSummary:
      type: object
      properties:
        id: { type: string }
        type: { type: string, enum: [train], example: train }
        name: { type: string, example: South Coast to London Waterloo }
        operator: { type: string, nullable: true }
        service_class: { type: string, nullable: true, example: Standard }
        direction: { type: string, enum: [outbound, return], example: outbound }
        currency: { type: string, example: GBP }
        country: { type: string, nullable: true }
        country_code: { type: string, nullable: true }
        region: { type: string, nullable: true }
        city: { type: string, nullable: true }
        terminal: { type: string, nullable: true, description: The single destination station. }
        start_point: { type: string, nullable: true, description: Origin (the destination on a return). }
        destination: { type: string, nullable: true }
        location:
          description: Coordinates of the destination (sourced via Locate stops).
          oneOf:
            - type: object
              properties: { latitude: { type: number }, longitude: { type: number } }
            - { type: 'null' }
        station_count: { type: integer, example: 4 }
        departure_count: { type: integer, example: 12 }
        upcoming_count: { type: integer, example: 8 }
        next_departure: { type: string, format: date, nullable: true }
        price_from:
          oneOf:
            - { $ref: '#/components/schemas/Money' }
            - { type: 'null' }
        booking_cutoff_hours: { type: integer, nullable: true }
        distance_km: { type: number, nullable: true }

    TrainDeparture:
      type: object
      description: One dated run of a train service - the unit of availability and inventory.
      properties:
        id: { type: string }
        departure_date: { type: string, format: date }
        status: { type: string, enum: [available, guaranteed, on_request, closed, cancelled] }
        season_id: { type: string, nullable: true }
        capacity: { type: integer, nullable: true, description: 'Seats; null means unlimited.' }
        seats_left: { type: integer, nullable: true }
        min_to_run: { type: integer, nullable: true }
        bookable: { type: boolean }
        price_from:
          oneOf:
            - { $ref: '#/components/schemas/Money' }
            - { type: 'null' }

    Train:
      type: object
      properties:
        id: { type: string }
        type: { type: string, enum: [train] }
        name: { type: string }
        operator: { type: string, nullable: true }
        service_class: { type: string, nullable: true }
        direction: { type: string, enum: [outbound, return] }
        currency: { type: string }
        country: { type: string, nullable: true }
        country_code: { type: string, nullable: true }
        region: { type: string, nullable: true }
        city: { type: string, nullable: true }
        location:
          oneOf:
            - type: object
              properties:
                latitude: { type: number }
                longitude: { type: number }
            - { type: 'null' }
        terminal: { type: string, nullable: true }
        start_point: { type: string, nullable: true }
        destination: { type: string, nullable: true }
        description: { type: string }
        images:
          type: array
          items: { $ref: '#/components/schemas/Image' }
        booking_cutoff_hours: { type: integer, nullable: true }
        valid_from: { type: string, format: date, nullable: true }
        valid_to: { type: string, format: date, nullable: true }
        stations:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              name: { type: string }
              board_time: { type: string, nullable: true, example: '07:30' }
              latitude: { type: number, nullable: true }
              longitude: { type: number, nullable: true }
        passenger_types:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              name: { type: string }
              min_age: { type: integer, nullable: true }
              max_age: { type: integer, nullable: true }
        seasons:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              name: { type: string, nullable: true }
              start_date: { type: string, format: date }
              end_date: { type: string, format: date }
        departures:
          type: array
          items: { $ref: '#/components/schemas/TrainDeparture' }

    TrainStation:
      type: object
      description: A priced, bookable boarding station for a departure, with its per-seat prices.
      properties:
        rate_reference: { type: string, description: 'Carry this, with the chosen passengers, into POST /bookings/quote.' }
        station:
          type: object
          properties:
            id: { type: string }
            name: { type: string }
            board_time: { type: string, nullable: true }
            latitude: { type: number, nullable: true }
            longitude: { type: number, nullable: true }
        passenger_types:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              name: { type: string }
              min_age: { type: integer, nullable: true }
              max_age: { type: integer, nullable: true }
              price_per_seat: { $ref: '#/components/schemas/Money' }
              quantity: { type: integer, description: Present when quantities were supplied. }
              price: { $ref: '#/components/schemas/Money' }
        price_from: { $ref: '#/components/schemas/Money' }
        price:
          description: The basket total, present when quantities were supplied.
          oneOf:
            - { $ref: '#/components/schemas/Money' }
            - { type: 'null' }
        available: { type: boolean }

    TrainAvailabilityRequest:
      type: object
      description: Pin the departure by departure_id or departure_date. passengers is optional (omit to see per-seat prices only).
      properties:
        departure_id: { type: string }
        departure_date: { type: string, format: date, example: '2026-07-04' }
        passengers:
          type: array
          items:
            type: object
            required: [passenger_type_id, qty]
            properties:
              passenger_type_id: { type: string }
              qty: { type: integer, minimum: 1 }

    TrainAvailabilityResponse:
      type: object
      properties:
        train_id: { type: string }
        departure: { $ref: '#/components/schemas/TrainDeparture' }
        currency: { type: string, nullable: true }
        reason: { type: string, description: 'When stations is empty, why (e.g. not_on_sale, within_cutoff, on_request, on_stop_sale, cancelled, sold_out, insufficient_seats, departed, no_boarding_point_available). insufficient_seats means the passengers you sent exceed the seats left; no_boarding_point_available means every station dropped out on its own.' }
        stations:
          type: array
          items: { $ref: '#/components/schemas/TrainStation' }

    TrainAvailabilitySearchRequest:
      type: object
      description: 'Search bookable train departures across all services by geography, date and party. Provide a boarding and/or destination filter; at least a date is required.'
      required: [date]
      properties:
        date: { type: string, format: date, example: '2026-07-04' }
        boarding:
          description: Where the passenger boards (a station for an outbound service, the destination for a return).
          type: object
          properties:
            lat: { type: number }
            lon: { type: number }
            radius_km: { type: number, default: 25 }
        destination:
          description: Where the passenger alights (the destination for an outbound service, a station for a return).
          type: object
          properties:
            lat: { type: number }
            lon: { type: number }
            radius_km: { type: number, default: 25 }
        passengers:
          type: object
          properties:
            adults: { type: integer, minimum: 0 }
            children: { type: integer, minimum: 0 }
            infants: { type: integer, minimum: 0 }
        page: { type: integer, minimum: 1, default: 1 }
        page_size: { type: integer, minimum: 1, maximum: 100, default: 20 }

    TrainAvailabilityResult:
      type: object
      description: One matching service with its bookable, in-radius stations for the date and party.
      properties:
        id: { type: string }
        name: { type: string }
        direction: { type: string, enum: [outbound, return] }
        operator: { type: string, nullable: true }
        service_class: { type: string, nullable: true }
        currency: { type: string }
        terminal: { type: string, nullable: true }
        location:
          oneOf:
            - type: object
              properties: { latitude: { type: number }, longitude: { type: number } }
            - { type: 'null' }
        start_point: { type: string, nullable: true }
        destination: { type: string, nullable: true }
        distance_km: { type: number, nullable: true, description: Distance from the boarding search point. }
        departure: { $ref: '#/components/schemas/TrainDeparture' }
        stations:
          type: array
          items: { $ref: '#/components/schemas/TrainStation' }
        price_from:
          oneOf:
            - { $ref: '#/components/schemas/Money' }
            - { type: 'null' }
        product:
          description: The full service detail, exactly as GET /v1/trains/{train_id} returns it.
          allOf:
            - { $ref: '#/components/schemas/Train' }

    TrainSearchRequest:
      description: 'Catalogue filters for train services. Every field is optional. Multi-value fields accept an array or a comma-separated string.'
      type: object
      properties:
        ids: { type: array, items: { type: string } }
        country: { type: array, items: { type: string } }
        region: { type: array, items: { type: string } }
        city: { type: array, items: { type: string } }
        operator: { type: array, items: { type: string } }
        near: { $ref: '#/components/schemas/NearFilter' }
        q: { type: string }
        sort: { type: string, enum: [name, distance] }
        page: { type: integer, minimum: 1, default: 1 }
        page_size: { type: integer, minimum: 1, maximum: 100, default: 20 }

    FlightAirport:
      type: object
      properties:
        id: { type: string }
        iata: { type: string, nullable: true, example: LHR }
        name: { type: string, nullable: true }
        city: { type: string, nullable: true }
        country: { type: string, nullable: true }

    FlightSummary:
      type: object
      properties:
        id: { type: string }
        type: { type: string, enum: [flight], example: flight }
        airline_name: { type: string }
        airline_iata: { type: string, nullable: true }
        supplier_name: { type: string, nullable: true }
        currency: { type: string, example: GBP }
        route_count: { type: integer }
        routes:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              trip_kind: { type: string, enum: [direct, connecting] }
              origin: { $ref: '#/components/schemas/FlightAirport' }
              dest: { $ref: '#/components/schemas/FlightAirport' }
        countries:
          type: array
          items: { type: string }
        valid_from: { type: string, format: date, nullable: true }
        valid_to: { type: string, format: date, nullable: true }
        booking_cutoff_hours: { type: integer, nullable: true }

    FlightLegDef:
      type: object
      properties:
        sequence: { type: integer }
        flight_number: { type: string, example: TX101 }
        from: { $ref: '#/components/schemas/FlightAirport' }
        to: { $ref: '#/components/schemas/FlightAirport' }
        dep_time: { type: string, nullable: true, example: '09:00' }
        arr_time: { type: string, nullable: true, example: '19:00' }
        day_offset: { type: integer, description: 'Arrival day relative to departure (0 same day, 1 next day).' }
        duration_min: { type: integer, nullable: true }
        aircraft_id: { type: string, nullable: true }
        connection_min: { type: integer, nullable: true }
        schedule:
          type: object
          properties:
            type: { type: string, enum: [recurring, dated] }
            date: { type: string, format: date, nullable: true, description: 'dated only' }
            days_of_week: { type: array, items: { type: integer }, description: 'recurring only; 0=Sunday .. 6=Saturday' }
            season:
              type: object
              properties:
                start: { type: string, format: date, nullable: true }
                end: { type: string, format: date, nullable: true }

    FlightCabinSummary:
      type: object
      properties:
        code: { type: string, enum: [Y, W, J, F] }
        name: { type: string }
        columns: { type: array, items: { type: string } }
        aisles_after: { type: array, items: { type: string } }
        rows: { type: array, items: { type: integer } }
        seat_count: { type: integer, description: Sellable seats in this cabin. }

    FlightAircraftSummary:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        registration: { type: string, nullable: true }
        cabins:
          type: array
          items: { $ref: '#/components/schemas/FlightCabinSummary' }
        seats:
          type: object
          properties:
            total: { type: integer }
            by_cabin: { type: object, additionalProperties: { type: integer } }

    FlightExtra:
      type: object
      properties:
        id: { type: string }
        type: { type: string, nullable: true }
        name: { type: string }
        charge_basis: { type: string, nullable: true, enum: [per_passenger, per_segment, per_booking] }
        price:
          oneOf:
            - { $ref: '#/components/schemas/Money' }
            - { type: 'null' }
        payable: { type: string, nullable: true, enum: [prepaid, local] }
        max_qty: { type: integer, nullable: true }

    Flight:
      type: object
      properties:
        id: { type: string }
        type: { type: string, enum: [flight], example: flight }
        airline_name: { type: string }
        airline_iata: { type: string, nullable: true }
        supplier_name: { type: string, nullable: true }
        currency: { type: string, example: GBP }
        valid_from: { type: string, format: date, nullable: true }
        valid_to: { type: string, format: date, nullable: true }
        booking_cutoff_hours: { type: integer, nullable: true }
        baggage_notes: { type: string }
        fare_rules_notes: { type: string }
        cancellation_notes: { type: string }
        terms_notes: { type: string }
        passenger_ages:
          type: object
          properties:
            child_min: { type: integer, nullable: true }
            child_max: { type: integer, nullable: true }
            infant_max: { type: integer, nullable: true }
        airports:
          type: array
          items: { $ref: '#/components/schemas/FlightAirport' }
        aircraft:
          type: array
          items: { $ref: '#/components/schemas/FlightAircraftSummary' }
        routes:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              trip_kind: { type: string, enum: [direct, connecting] }
              origin: { $ref: '#/components/schemas/FlightAirport' }
              dest: { $ref: '#/components/schemas/FlightAirport' }
              cabins: { type: array, items: { type: string, enum: [Y, W, J, F] } }
              legs:
                type: array
                items: { $ref: '#/components/schemas/FlightLegDef' }
        extras:
          type: array
          items: { $ref: '#/components/schemas/FlightExtra' }

    FlightSegment:
      type: object
      properties:
        flight_number: { type: string }
        from: { $ref: '#/components/schemas/FlightAirport' }
        to: { $ref: '#/components/schemas/FlightAirport' }
        depart:
          type: object
          properties:
            date: { type: string, format: date }
            time: { type: string, nullable: true }
            at: { type: string, nullable: true, description: 'A naive local timestamp, for ordering only.' }
        arrive:
          type: object
          properties:
            date: { type: string, format: date }
            time: { type: string, nullable: true }
            at: { type: string, nullable: true }
        duration_min: { type: integer, nullable: true }
        day_offset: { type: integer }

    FlightFareLine:
      type: object
      properties:
        passenger_type: { type: string, enum: [adult, child, infant] }
        count: { type: integer }
        price_per_passenger: { $ref: '#/components/schemas/Money' }
        price: { $ref: '#/components/schemas/Money' }
        fare_basis: { type: string, nullable: true }
        baggage_allowance: { type: string, nullable: true }
        refundable: { type: boolean }

    FlightDirection:
      type: object
      description: One direction of a journey - its flown segments and per-passenger fares.
      properties:
        route_id: { type: string }
        trip_kind: { type: string, enum: [direct, connecting] }
        stops: { type: integer }
        depart:
          type: object
          properties:
            date: { type: string, format: date }
            time: { type: string, nullable: true }
            airport:
              oneOf:
                - { $ref: '#/components/schemas/FlightAirport' }
                - { type: 'null' }
        arrive:
          type: object
          properties:
            date: { type: string, format: date }
            time: { type: string, nullable: true }
            airport:
              oneOf:
                - { $ref: '#/components/schemas/FlightAirport' }
                - { type: 'null' }
        segments:
          type: array
          items: { $ref: '#/components/schemas/FlightSegment' }
        fares:
          type: array
          items: { $ref: '#/components/schemas/FlightFareLine' }
        subtotal: { $ref: '#/components/schemas/Money' }
        baggage_allowance: { type: string, nullable: true }
        refundable: { type: boolean }
        seats_available: { type: integer, nullable: true, description: 'Null when no seat map limits the cabin.' }
        on_stop_sale: { type: boolean }

    FlightOption:
      type: object
      description: A priced, bookable journey (one-way or return) with its rate_reference.
      properties:
        rate_reference: { type: string, description: 'Carry this, with the passengers, into POST /bookings/quote.' }
        trip_type: { type: string, enum: [one_way, return] }
        cabin: { type: string, enum: [Y, W, J, F] }
        cabin_name: { type: string }
        outbound: { $ref: '#/components/schemas/FlightDirection' }
        return:
          oneOf:
            - { $ref: '#/components/schemas/FlightDirection' }
            - { type: 'null' }
        price: { $ref: '#/components/schemas/Money' }
        available: { type: boolean }

    FlightAvailabilityRequest:
      type: object
      required: [origin, dest, date, adults]
      properties:
        origin: { type: string, description: Origin airport IATA code or id., example: LHR }
        dest: { type: string, description: Destination airport IATA code or id., example: JFK }
        date: { type: string, format: date, example: '2026-07-10' }
        return_date: { type: string, format: date, nullable: true }
        cabin: { type: string, enum: [Y, W, J, F], description: Optional; limit to one cabin. }
        adults: { type: integer, minimum: 1, default: 1 }
        children: { type: integer, minimum: 0, default: 0 }
        infants: { type: integer, minimum: 0, default: 0 }

    FlightAvailabilityResponse:
      type: object
      properties:
        flight_id: { type: string }
        origin: { type: string, nullable: true }
        dest: { type: string, nullable: true }
        outbound_date: { type: string, format: date, nullable: true }
        return_date: { type: string, format: date, nullable: true }
        trip_type: { type: string, enum: [one_way, return] }
        currency: { type: string, nullable: true }
        reason: { type: string, description: 'When options is empty, why (e.g. not_on_sale, unavailable, no_routes).' }
        options:
          type: array
          items: { $ref: '#/components/schemas/FlightOption' }
        extras:
          type: array
          items: { $ref: '#/components/schemas/FlightExtra' }

    FlightSearchRequest:
      description: 'Catalogue filters for flight deals. Every field is optional. Multi-value fields accept an array or a comma-separated string.'
      type: object
      properties:
        ids: { type: array, items: { type: string } }
        airline: { type: array, items: { type: string }, description: 'Airline names or IATA codes.' }
        origin: { type: array, items: { type: string }, description: 'Origin airport IATA codes.' }
        dest: { type: array, items: { type: string }, description: 'Destination airport IATA codes.' }
        country: { type: array, items: { type: string } }
        city: { type: array, items: { type: string } }
        supplier: { type: array, items: { type: string } }
        q: { type: string }
        sort: { type: string, enum: [name, price] }
        page: { type: integer, minimum: 1, default: 1 }
        page_size: { type: integer, minimum: 1, maximum: 100, default: 20 }

    FlightAvailabilitySearchRequest:
      description: 'Bulk priced flight availability for one route and date across many deals. origin, dest, date and at least one adult are required.'
      type: object
      required: [origin, dest, date, adults]
      properties:
        origin: { type: string, example: LHR }
        dest: { type: string, example: JFK }
        date: { type: string, format: date }
        return_date: { type: string, format: date }
        cabin: { type: string, enum: [Y, W, J, F] }
        adults: { type: integer, minimum: 1, default: 1 }
        children: { type: integer, minimum: 0 }
        infants: { type: integer, minimum: 0 }
        airline: { type: array, items: { type: string } }
        ids: { type: array, items: { type: string } }
        sort: { type: string, enum: [price, name] }
        page: { type: integer, minimum: 1, default: 1 }
        page_size: { type: integer, minimum: 1, maximum: 100, default: 20 }

    FlightLeadDirection:
      type: object
      properties:
        route_id: { type: string }
        stops: { type: integer }
        depart:
          type: object
          properties:
            date: { type: string, format: date }
            time: { type: string, nullable: true }
            airport:
              oneOf:
                - { $ref: '#/components/schemas/FlightAirport' }
                - { type: 'null' }
        arrive:
          type: object
          properties:
            date: { type: string, format: date }
            time: { type: string, nullable: true }
            airport:
              oneOf:
                - { $ref: '#/components/schemas/FlightAirport' }
                - { type: 'null' }
        flight_numbers: { type: array, items: { type: string } }

    FlightAvailabilityResult:
      description: One flight deal with at least one bookable journey, and its lead (cheapest) option.
      type: object
      properties:
        product_id: { type: string }
        type: { type: string, enum: [flight] }
        airline_name: { type: string }
        airline_iata: { type: string, nullable: true }
        supplier_name: { type: string, nullable: true }
        currency: { type: string }
        price_from: { $ref: '#/components/schemas/Money' }
        option_count: { type: integer }
        trip_type: { type: string, enum: [one_way, return] }
        lead_option:
          type: object
          properties:
            rate_reference: { type: string }
            cabin: { type: string, enum: [Y, W, J, F] }
            cabin_name: { type: string }
            trip_type: { type: string, enum: [one_way, return] }
            outbound: { $ref: '#/components/schemas/FlightLeadDirection' }
            return:
              oneOf:
                - { $ref: '#/components/schemas/FlightLeadDirection' }
                - { type: 'null' }
            price: { $ref: '#/components/schemas/Money' }
        options:
          type: array
          description: Every bookable cabin and routing priced, each ready for /bookings/quote.
          items: { $ref: '#/components/schemas/FlightOption' }
        product:
          description: The full deal detail, exactly as GET /v1/flights/{flight_id} returns it.
          allOf:
            - { $ref: '#/components/schemas/Flight' }

    FlightSeatmapRequest:
      type: object
      description: Send a rate_reference, or route_id + date (with optional cabin and return route/date).
      properties:
        rate_reference: { type: string }
        route_id: { type: string }
        date: { type: string, format: date }
        cabin: { type: string, enum: [Y, W, J, F] }
        return_route_id: { type: string }
        return_date: { type: string, format: date }

    FlightSeat:
      type: object
      properties:
        code: { type: string, example: 12A }
        cabin: { type: string, enum: [Y, W, J, F] }
        row: { type: integer }
        col: { type: string }
        type: { type: string, enum: [window, middle, aisle, seat] }
        attributes: { type: array, items: { type: string }, description: 'e.g. extra_legroom, exit_row' }
        exit_row: { type: boolean }
        fee:
          oneOf:
            - { $ref: '#/components/schemas/Money' }
            - { type: 'null' }
        price_label: { type: string, nullable: true, example: Extra legroom, description: 'The operator''s own name for this paid seat, when the fee was set as a named set (e.g. Extra legroom). Null for unnamed or free seats. Use it to label the seat in your picker.' }
        available: { type: boolean, description: 'False when blocked or already taken.' }

    FlightSeatmapSegment:
      type: object
      properties:
        leg_id: { type: string }
        flight_number: { type: string }
        from: { $ref: '#/components/schemas/FlightAirport' }
        to: { $ref: '#/components/schemas/FlightAirport' }
        date: { type: string, format: date }
        aircraft:
          oneOf:
            - type: object
              properties:
                id: { type: string }
                name: { type: string }
            - { type: 'null' }
        cabin: { type: string, nullable: true }
        cabins:
          type: array
          items: { $ref: '#/components/schemas/FlightCabinSummary' }
        seats:
          type: array
          items: { $ref: '#/components/schemas/FlightSeat' }
        seats_available: { type: integer }
        has_seat_map: { type: boolean }

    FlightSeatmapResponse:
      type: object
      properties:
        flight_id: { type: string }
        currency: { type: string }
        trip_type: { type: string, enum: [one_way, return] }
        cabin: { type: string, nullable: true }
        outbound:
          type: object
          properties:
            route_id: { type: string }
            date: { type: string, format: date }
            segments:
              type: array
              items: { $ref: '#/components/schemas/FlightSeatmapSegment' }
        return:
          oneOf:
            - type: object
              properties:
                route_id: { type: string }
                date: { type: string, format: date }
                segments:
                  type: array
                  items: { $ref: '#/components/schemas/FlightSeatmapSegment' }
            - { type: 'null' }

    TicketSeatmapRequest:
      type: object
      description: Pin the departure by departure_id or departure_date.
      properties:
        departure_id: { type: string }
        departure_date: { type: string, format: date }

    TicketSeatType:
      type: object
      description: >
        A named seat type (zone), e.g. Standard or First, and the shape of its
        block of seats. The surcharge is per seat and is carried on each seat
        (TicketSeat.fee), not here: seats within one zone can be priced apart.
      properties:
        code: { type: string, example: FIRST }
        name: { type: string, example: First }
        rows:
          type: array
          description: The row numbers in this zone, in order.
          items: { type: integer }
          example: [1, 2]
        columns:
          type: array
          description: Seat letters across a row, in order.
          items: { type: string }
          example: [A, B, C, D]
        aisles_after:
          type: array
          description: Draw an aisle after these columns. Letters, matching columns, not positions.
          items: { type: string }
          example: [B]
        seat_count: { type: integer, description: Sellable seats in this zone, excluding any blocked., example: 8 }

    TicketSeat:
      type: object
      properties:
        code: { type: string, example: 12A }
        row: { type: integer }
        col: { type: string }
        type: { type: string, enum: [window, middle, aisle, seat] }
        seat_type: { type: string, description: 'The seat type (zone) code this seat belongs to.', example: FIRST }
        seat_type_name: { type: string, example: First }
        attributes: { type: array, items: { type: string } }
        fee:
          oneOf:
            - { $ref: '#/components/schemas/Money' }
            - { type: 'null' }
        available: { type: boolean, description: 'False when blocked or already taken.' }

    CoachSeatmapResponse:
      type: object
      properties:
        coach_id: { type: string }
        departure: { $ref: '#/components/schemas/CoachDeparture' }
        currency: { type: string, nullable: true }
        has_seat_map: { type: boolean, description: 'False when the route has no seat plan (sold by seat count only).' }
        seat_types:
          type: array
          items: { $ref: '#/components/schemas/TicketSeatType' }
        seats:
          type: array
          items: { $ref: '#/components/schemas/TicketSeat' }

    TrainSeatmapResponse:
      type: object
      properties:
        train_id: { type: string }
        departure: { $ref: '#/components/schemas/TrainDeparture' }
        currency: { type: string, nullable: true }
        has_seat_map: { type: boolean, description: 'False when the service has no seat plan (sold by seat count only).' }
        seat_types:
          type: array
          items: { $ref: '#/components/schemas/TicketSeatType' }
        seats:
          type: array
          items: { $ref: '#/components/schemas/TicketSeat' }
