openapi: 3.1.0
info:
  title: Trekko Hotel API (Partner / Client)
  version: "2026-06-18"
  description: |
    B2B partner API for booking accommodation through Trekko using an **API key**
    (instead of the interactive web login). Every booking you confirm is
    attributed to **your agency** in Trekko's CRM, and prices already include
    your configured **markup** (applied at egress on the Trekko side).

    ### Status / what's live (2026-06-18)
    The endpoints under **Search**, **Availability** and **Bookings** (search,
    availability, checkout, the booking-payment step, confirm, cancel,
    list_bookings, get_booking) are live in production and exercised by the first
    partner (StayTick = **agency 50**). The **Sandbox (test mode)** surface
    (`/core/sandbox/...`) is live for self-serve testing. The **Account**
    (balance/savings) and **Webhooks** endpoints back Trekko's automated
    rebooking (SBPO) money-flow, which is described in detail in
    `REBOOKING_FOR_PARTNERS.md`; treat the savings/webhook payloads there as the
    forward-looking design.

    Published client SDKs (`pip install trekko`,
    `npm i @staytick/trekko-client`) and the admin provisioning endpoint
    `/admin/agencies/<id>/api_credentials` are **not yet published / deployed** —
    contact Trekko for credentials rather than relying on them.

    ### Authentication / Getting started
    Send credentials with **either** header form (the first is preferred):

    - Single header: `Authorization: ApiKey key:secret`
    - Split headers: `X-Api-Key: key` and `X-Api-Secret: secret`

    Keys are prefixed `trk_live_` (production, admin-provisioned) or `trk_test_`
    (sandbox, issued instantly by `POST /core/sandbox/signup`). The **secret is
    shown only once** and is stored hashed server-side — store it on receipt.
    Rotation = Trekko re-issues the pair; revocation = the key is deactivated
    (`is_active=false`). A bad/inactive key or wrong secret returns
    `401 invalid_api_key`.

    ### Conventions
    - All requests and responses are `application/json`.
    - **`Accept-Language` is OPTIONAL** and defaults to `en`. It is only rejected
      when **explicitly set to an unsupported value** (allowed: `en`, `es`, `fr`,
      `de`, `it`, `pt`); omitting it is fine.
    - Base path is `/core/...` (there is no `/api/` prefix on the public host).
    - Errors use a stable envelope (`error.code` is machine-readable and stable;
      `error.message` is human-readable and may change). `X-Request-Id` is
      currently returned on **error** responses — quote it to support.
  contact:
    name: Trekko Developer Support
    url: https://trekko.co
  license:
    name: Proprietary
    identifier: LicenseRef-Trekko-Proprietary

servers:
  - url: https://api.trekko.co/core
    description: Production
  - url: https://api.trekko.co/core/sandbox
    description: Sandbox (test mode — trk_test_ keys, no real charges)

security:
  - ApiKeyAuthorization: []
  - ApiKeyId: []
    ApiKeySecret: []

tags:
  - name: Search
    description: Resolve cities, areas, landmarks and accommodations.
  - name: Availability
    description: Live bookable rate plans for a stay.
  - name: Bookings
    description: Checkout, pay, confirm, cancel, list and look up bookings.
  - name: Account
    description: Partner balance and rebooking-savings ledger.
  - name: Webhooks
    description: Register signed outbound webhooks (e.g. booking.rebooked).
  - name: Sandbox (test mode)
    description: Self-serve test surface under /core/sandbox; trk_test_ keys, no real vendors/money.

paths:
  /get_cities_list:
    get:
      tags: [Search]
      summary: List known cities (optional helper)
      description: |
        Returns the platform's known cities to help resolve a city for
        searching. **No API-key auth** is required (public/AllowAny). Pass an
        optional `country_id` to filter to one country.
      operationId: getCitiesList
      security: []
      parameters:
        - $ref: '#/components/parameters/AcceptLanguage'
        - name: country_id
          in: query
          required: false
          description: Restrict the list to a single country.
          schema: { type: integer }
      responses:
        '200':
          description: Cities list (a top-level JSON array).
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  properties:
                    id: { type: integer }
                    name: { type: string }
              example:
                - { id: 2, name: Madrid }
                - { id: 3, name: Barcelona }
                - { id: 4, name: Dubai }
        '204':
          description: No city data available.

  /accommodation/search:
    get:
      tags: [Search]
      summary: Text search for cities, areas, landmarks and accommodations
      description: |
        Free-text search. `search_by_type` indicates the entity type:
        `0` Accommodation · `1` City · `2` Area · `3` Landmark. Use the returned
        `search_by_id` / `search_by_type` to drive availability.
      operationId: searchAccommodation
      parameters:
        - $ref: '#/components/parameters/AcceptLanguage'
        - name: query
          in: query
          required: true
          description: Free-text search term, e.g. `Barcelona`.
          schema: { type: string }
          example: Barcelona
      responses:
        '200':
          description: Matching search results.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SearchResponse' }
              example:
                data:
                  - name: "Villa White Rock, Ban Amphur"
                    sub_name: ""
                    search_by_id: "1376114"
                    search_by_type: 0
                    latitude: 12.843385
                    longitude: 100.935948
                    landmarks: []
                    area: []
                  - name: "Ban Mai Subdistrict"
                    sub_name: "Thailand"
                    search_by_id: "57789"
                    search_by_type: 1
                    latitude: 19.288262
                    longitude: 99.765417
                    landmarks: []
                    area: []
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /accommodations/get_accommodation_availability:
    post:
      tags: [Availability]
      summary: Live availability + bookable rate plans for a stay
      description: |
        Returns bookable rate plans (prices **include your markup**) for a hotel
        over a date range. Carry every field you receive (`vendor`,
        `booking_code`, the price fields, `cancellation_policy`,
        `cancellation_policy_rules`, `room_codes`, `board_plan_text`) forward into
        checkout and confirm. `204` means no availability for that query.

        **Guest rules** (enforced server-side): `guests` is a flat list of
        `{id, age}`; guest ids must be unique; the booking needs at least one
        adult (age ≥ 18). `guests_room_distribution` assigns every guest id to
        exactly one room, and each room must contain at least one adult.
        `nationality` is OPTIONAL.
      operationId: getAccommodationAvailability
      parameters:
        - $ref: '#/components/parameters/AcceptLanguage'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/AvailabilityRequest' }
            example:
              accommodation_id: 1376114
              start_date: "2026-09-01"
              end_date: "2026-09-04"
              guests:
                - { id: 1, age: 34 }
                - { id: 2, age: 30 }
              guests_room_distribution:
                - { guest_ids: [1, 2] }
              nationality: ES
              currency_code: EUR
      responses:
        '200':
          description: Availability with one or more bookable options.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AvailabilityResponse' }
        '204':
          description: No availability for this query.
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '503': { $ref: '#/components/responses/ServiceUnavailable' }

  /accommodation/bookings/checkout:
    post:
      tags: [Bookings]
      summary: Re-validate the chosen option's price
      description: |
        Re-validates the selected option and returns the locked price plus the
        `booking_code` / `room_codes` / cancellation policy you carry into the
        payment + confirm steps. Send the guest list and room distribution again
        (same validation rules as availability). **Checkout does NOT return an
        `invoice_id`** — that comes from the payment step (see the lifecycle
        below).
      operationId: checkout
      parameters:
        - $ref: '#/components/parameters/AcceptLanguage'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CheckoutRequest' }
            example:
              accommodation_id: 1376114
              vendor: "src_3f9a2b"
              rate_plan_code: RP-ABC-123
              start_date: "2026-09-01"
              end_date: "2026-09-04"
              guests:
                - { id: 1, age: 34 }
                - { id: 2, age: 30 }
              guests_room_distribution:
                - { guest_ids: [1, 2] }
              currency_code: EUR
      responses:
        '200':
          description: Locked price + the fields to carry into payment/confirm.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CheckoutResponse' }
        '204':
          description: Option no longer bookable.
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /accommodation/bookings/pay_by_funds:
    post:
      tags: [Bookings]
      summary: Pay for the booking from your agency balance (issues the invoice)
      description: |
        The **payment step** that produces the `invoice_id` `confirm_booking`
        requires. Partners on a credit/prepaid account use this funds path:
        Trekko charges your virtual wallet / credit balance and returns an
        `invoice_id` plus the matching `payment_type`
        (`"Booking Payment by Funds"`). Pass that same `invoice_id` and
        `payment_type` into confirm.

        Other payment paths exist for web/card flows
        (`/accommodation/bookings/pay_by_own_card`,
        `/accommodation/bookings/pay_by_clients_card`,
        `/accommodation/bookings/pay_later`); each likewise yields an
        `invoice_id` + `payment_type`. Insufficient funds returns `400`.
      operationId: payByFunds
      parameters:
        - $ref: '#/components/parameters/AcceptLanguage'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amount]
              properties:
                amount: { type: number, example: 360.00, description: Amount to charge (the price you will confirm). }
                currency_code: { type: string, default: EUR }
            example: { amount: 360.00, currency_code: EUR }
      responses:
        '200':
          description: Payment succeeded; invoice issued.
          content:
            application/json:
              schema:
                type: object
                properties:
                  invoice_id: { type: integer, example: 24881 }
                  payment_type: { type: string, example: "Booking Payment by Funds" }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '503': { $ref: '#/components/responses/ServiceUnavailable' }

  /accommodation/bookings/confirm_booking:
    post:
      tags: [Bookings]
      summary: Confirm the booking (attributes it to your agency)
      description: |
        Creates the booking and attributes it to your agency in the CRM. Requires
        a paid `invoice_id` (from the payment step) and its matching
        `payment_type`; an unpaid/invalid invoice returns `402 payment_required`.

        **Guests:** `guests_information` is a SINGLE OBJECT carrying the lead
        guest plus an optional `guests` array of extra occupants. The lead guest
        is treated as `id == 1` and must be at least 18. `rooms_data` and
        `guests_room_distribution` are required. `room_codes` is required when the
        vendor is a specific source.

        Send a stable `idempotency_key` in the body **and** the `Idempotency-Key`
        header (same value): a retry/timeout then replays the original response
        instead of double-booking. The replayed response carries
        `Idempotency-Replayed: true`.
      operationId: confirmBooking
      parameters:
        - $ref: '#/components/parameters/AcceptLanguage'
        - name: Idempotency-Key
          in: header
          required: false
          description: Stable unique value (e.g. a UUID) per booking attempt. Remembered for 24h.
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ConfirmBookingRequest' }
      responses:
        '200':
          description: Booking confirmed.
          headers:
            Idempotency-Replayed:
              description: '`true` when this is a replay of a prior identical request.'
              schema: { type: boolean }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ConfirmBookingResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/PaymentRequired' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/BookingConflict' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '503': { $ref: '#/components/responses/BookingFailed' }

  /accommodation/bookings/cancel_booking:
    post:
      tags: [Bookings]
      summary: Cancel a booking
      description: |
        Cancels a booking, subject to the rate's cancellation policy / fees.
        `booking_number` is the **integer** booking number and
        `cancellation_reason` (non-empty) is required.
      operationId: cancelBooking
      parameters:
        - $ref: '#/components/parameters/AcceptLanguage'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [booking_number, cancellation_reason]
              properties:
                booking_number: { type: integer, example: 1045 }
                cancellation_reason: { type: string, minLength: 1, example: "Guest requested cancellation" }
      responses:
        '200':
          description: Cancellation accepted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  message: { type: string }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '503': { $ref: '#/components/responses/ServiceUnavailable' }

  /accommodation/bookings/list_bookings:
    get:
      tags: [Bookings]
      summary: List your agency's bookings (paginated)
      description: |
        Reads all filters from the **query string**. Returns the agency's
        bookings, newest first.
      operationId: listBookings
      parameters:
        - $ref: '#/components/parameters/AcceptLanguage'
        - { name: page_no, in: query, required: false, schema: { type: integer, minimum: 1, default: 1 } }
        - { name: items_per_page, in: query, required: false, schema: { type: integer, minimum: 1, default: 5 } }
        - { name: status, in: query, required: false, schema: { type: string, enum: [upcoming, completed, cancelled] } }
        - { name: check_in, in: query, required: false, schema: { type: string, format: date } }
        - { name: check_out, in: query, required: false, schema: { type: string, format: date } }
        - { name: booking_date, in: query, required: false, schema: { type: string, format: date } }
        - { name: booking_id, in: query, required: false, schema: { type: string } }
        - { name: guest_name, in: query, required: false, schema: { type: string } }
        - { name: country, in: query, required: false, schema: { type: string } }
        - { name: city, in: query, required: false, schema: { type: string } }
        - { name: currency_code, in: query, required: false, schema: { type: string } }
        - { name: travel_agency_ref, in: query, required: false, schema: { type: integer } }
        - { name: booking_type, in: query, required: false, schema: { type: string, enum: [Rebooked, Original] } }
        - { name: search_query, in: query, required: false, schema: { type: string } }
        - { name: agencies_internal_booking_ref, in: query, required: false, schema: { type: string } }
      responses:
        '200':
          description: A page of bookings.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ListBookingsResponse' }
        '204':
          description: No bookings found.
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /accommodation/bookings/get_booking:
    post:
      tags: [Bookings]
      summary: Resolve a stable booking_id (or booking_number) to its current state
      description: |
        Returns the CURRENT active booking for a stable `booking_id` (which
        survives SBPO rebooking) or an integer `booking_number`, including a
        `rebooking` block when the booking was rebooked to a cheaper rate. The
        partner stores `booking_id` and uses this to map it to the live
        `booking_number` (e.g. before cancelling). See REBOOKING_FOR_PARTNERS.md.
      operationId: getBooking
      parameters:
        - $ref: '#/components/parameters/AcceptLanguage'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                booking_id: { type: string, example: TRK-1045 }
                booking_number: { type: integer, example: 1045 }
            example: { booking_id: TRK-1045 }
      responses:
        '200':
          description: Current booking state (+ rebooking block if rebooked).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PartnerBooking' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /accommodation/account/balance:
    post:
      tags: [Account]
      summary: Partner account balance (virtual wallet or credit balance)
      operationId: accountBalance
      parameters:
        - $ref: '#/components/parameters/AcceptLanguage'
      responses:
        '200':
          description: Balance.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AccountBalance' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /accommodation/account/savings:
    post:
      tags: [Account]
      summary: Rebooking savings ledger (paginated, + lifetime totals)
      description: One row per SBPO rebooking — the "we saved you €X after booking" data.
      operationId: accountSavings
      parameters:
        - $ref: '#/components/parameters/AcceptLanguage'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                page_no: { type: integer, default: 1 }
                items_per_page: { type: integer, default: 20 }
      responses:
        '200':
          description: Savings ledger page.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SavingsLedger' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /accommodation/webhooks:
    post:
      tags: [Webhooks]
      summary: Register an outbound webhook endpoint (returns the signing secret once)
      description: |
        Register an HTTPS URL to receive signed events (e.g. `booking.rebooked`).
        The `signing_secret` is returned ONCE. Each delivery carries a
        `Trekko-Signature: t=<unix>,v1=<hex>` header where
        `v1 = HMAC_SHA256(secret, "<t>.<raw_body>")`. URLs resolving to
        private/loopback/metadata addresses are rejected.
      operationId: registerWebhook
      parameters:
        - $ref: '#/components/parameters/AcceptLanguage'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url: { type: string, format: uri, example: "https://example.com/trekko/webhooks" }
                events: { type: string, description: "Comma-separated event types, or '*'.", example: "booking.rebooked" }
      responses:
        '201':
          description: Endpoint registered; signing secret shown once.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: integer }
                  url: { type: string }
                  events: { type: string }
                  signing_secret: { type: string, example: "whsec_..." }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
    get:
      tags: [Webhooks]
      summary: List your webhook endpoints (never returns the secret)
      operationId: listWebhooks
      parameters:
        - $ref: '#/components/parameters/AcceptLanguage'
      responses:
        '200':
          description: Endpoints.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: integer }
                        url: { type: string }
                        events: { type: string }
                        is_active: { type: boolean }
        '401': { $ref: '#/components/responses/Unauthorized' }

  # ── Self-serve developer SANDBOX (test mode) ───────────────────────────────
  # Mounted under https://api.trekko.co/core/sandbox. Paths below are written
  # relative to the PRODUCTION server (https://api.trekko.co/core) so they
  # resolve correctly regardless of which server entry a tool picks.
  /sandbox/signup:
    post:
      tags: [Sandbox (test mode)]
      summary: Create a sandbox account + test API key (shown once)
      description: |
        Public, no-auth self-serve signup. Returns a `trk_test_*` key/secret pair
        (secret shown ONCE) plus the sandbox base URL. Use the returned key to
        call any `/sandbox/...` endpoint and run the full
        search→availability→checkout→confirm→(trigger_rebooking)→cancel loop with
        no real vendors, money or production data.
      operationId: sandboxSignup
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                email: { type: string, format: email }
                company_name: { type: string }
            example: { email: "dev@example.com", company_name: "Example Travel" }
      responses:
        '201':
          description: Sandbox account created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  sandbox_account_id: { type: integer }
                  api_key: { type: string, example: "trk_test_abcdef0123456789" }
                  api_secret: { type: string, description: Shown only once. }
                  mode: { type: string, example: test }
                  base_url: { type: string, example: "https://api.trekko.co/core/sandbox" }
                  auth_header_example: { type: string }
                  note: { type: string }
        '400': { $ref: '#/components/responses/BadRequest' }

  /sandbox/accommodation/search:
    post:
      tags: [Sandbox (test mode)]
      summary: Sandbox text search (test mode)
      description: Mirrors the live search contract; backed by the isolated sandbox engine. Accepts `query` as a query param or in the body.
      operationId: sandboxSearch
      parameters:
        - $ref: '#/components/parameters/AcceptLanguage'
        - { name: query, in: query, required: false, schema: { type: string }, example: Barcelona }
      responses:
        '200': { description: Sandbox search results. }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /sandbox/accommodations/get_accommodation_availability:
    post:
      tags: [Sandbox (test mode)]
      summary: Sandbox availability (test mode)
      operationId: sandboxAvailability
      parameters:
        - $ref: '#/components/parameters/AcceptLanguage'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/AvailabilityRequest' }
      responses:
        '200': { description: Sandbox availability. }
        '204': { description: No availability. }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /sandbox/accommodation/bookings/checkout:
    post:
      tags: [Sandbox (test mode)]
      summary: Sandbox checkout (test mode)
      operationId: sandboxCheckout
      parameters:
        - $ref: '#/components/parameters/AcceptLanguage'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CheckoutRequest' }
      responses:
        '200': { description: Sandbox locked price. }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /sandbox/accommodation/bookings/confirm_booking:
    post:
      tags: [Sandbox (test mode)]
      summary: Sandbox confirm (test mode)
      operationId: sandboxConfirm
      parameters:
        - $ref: '#/components/parameters/AcceptLanguage'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ConfirmBookingRequest' }
      responses:
        '200': { description: Sandbox booking confirmed. }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /sandbox/accommodation/bookings/cancel_booking:
    post:
      tags: [Sandbox (test mode)]
      summary: Sandbox cancel (test mode)
      operationId: sandboxCancel
      parameters:
        - $ref: '#/components/parameters/AcceptLanguage'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                booking_number: { type: integer }
                booking_id: { type: string }
      responses:
        '200': { description: Sandbox cancellation. }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /sandbox/accommodation/bookings/list_bookings:
    post:
      tags: [Sandbox (test mode)]
      summary: Sandbox list bookings (test mode)
      operationId: sandboxListBookings
      parameters:
        - $ref: '#/components/parameters/AcceptLanguage'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                page_no: { type: integer, default: 1 }
                items_per_page: { type: integer, default: 20 }
      responses:
        '200': { description: Sandbox bookings page. }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /sandbox/accommodation/bookings/get_booking:
    post:
      tags: [Sandbox (test mode)]
      summary: Sandbox get booking (test mode)
      operationId: sandboxGetBooking
      parameters:
        - $ref: '#/components/parameters/AcceptLanguage'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                booking_id: { type: string }
                booking_number: { type: integer }
      responses:
        '200': { description: Sandbox booking state. }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /sandbox/accommodation/bookings/trigger_rebooking:
    post:
      tags: [Sandbox (test mode)]
      summary: Simulate an SBPO rebooking (test only)
      description: Simulate SBPO rebooking a confirmed sandbox booking to a cheaper rate — applies the real money model, credits savings, and fires `booking.rebooked`.
      operationId: sandboxTriggerRebooking
      parameters:
        - $ref: '#/components/parameters/AcceptLanguage'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                booking_id: { type: string }
                booking_number: { type: integer }
      responses:
        '200': { description: Rebooking simulated. }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /sandbox/accommodation/account/balance:
    post:
      tags: [Sandbox (test mode)]
      summary: Sandbox account balance (test mode)
      operationId: sandboxAccountBalance
      parameters:
        - $ref: '#/components/parameters/AcceptLanguage'
      responses:
        '200': { description: Sandbox balance. }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /sandbox/accommodation/account/savings:
    post:
      tags: [Sandbox (test mode)]
      summary: Sandbox savings ledger (test mode)
      operationId: sandboxAccountSavings
      parameters:
        - $ref: '#/components/parameters/AcceptLanguage'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                page_no: { type: integer, default: 1 }
                items_per_page: { type: integer, default: 20 }
      responses:
        '200': { description: Sandbox savings page. }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /sandbox/accommodation/webhooks:
    post:
      tags: [Sandbox (test mode)]
      summary: Register the sandbox webhook (signing secret shown once)
      operationId: sandboxRegisterWebhook
      parameters:
        - $ref: '#/components/parameters/AcceptLanguage'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url: { type: string, format: uri }
      responses:
        '201': { description: Sandbox webhook registered. }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /account/production/status:
    post:
      tags: [Production onboarding]
      summary: Production onboarding & funding status
      description: |
        Read-only. Authenticate with your **test key** (`trk_test_*`). Returns
        whether your account is promoted/live, the required minimum deposit, and
        your current funding (wallet deposit + credit line). See
        SELF_SERVE_PRODUCTION.md.
      operationId: productionStatus
      responses:
        '200':
          description: Onboarding + funding state.
          content:
            application/json:
              schema:
                type: object
                properties:
                  mode: { type: string, enum: [test, pending_deposit, ready, live] }
                  production_active: { type: boolean }
                  deposit_required: { type: number, example: 3000 }
                  available_balance: { type: number, description: Wallet deposit + credit line (EUR). }
                  gap: { type: number, description: Amount left to fund (EUR). }
                  currency: { type: string, example: EUR }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { description: Account not found. }

  /account/production/activate:
    post:
      tags: [Production onboarding]
      summary: Activate production (mint live key once funded)
      description: |
        Authenticate with your **test key**. Promotes your account to a real
        agency and mints your `trk_live_` key (secret shown ONCE) — **only once**
        your wallet deposit + credit line reach the minimum (€3,000 by default).
        There is no payment in this call: fund your wallet first via the normal
        channels (card top-up or bank transfer). Until funded it returns
        `pending_deposit` with how much is left.
      operationId: productionActivate
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                email: { type: string, format: email, description: Required only if not set at signup. }
                company_name: { type: string }
            example: { email: "dev@yourcompany.com" }
      responses:
        '200':
          description: |
            Either `mode: "live"` (with the live key + secret, shown once) or
            `mode: "pending_deposit"` (with the funding gap and how to fund).
          content:
            application/json:
              schema:
                type: object
                properties:
                  mode: { type: string, enum: [live, pending_deposit] }
                  production_active: { type: boolean }
                  agency_id: { type: integer }
                  api_key: { type: string, example: "trk_live_…", description: Present when mode=live. }
                  api_secret: { type: string, description: Present when mode=live. Shown only once. }
                  base_url: { type: string, example: "https://api.trekko.co/core" }
                  deposit_required: { type: number, example: 3000 }
                  available_balance: { type: number }
                  gap: { type: number }
                  currency: { type: string, example: EUR }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }

components:
  securitySchemes:
    ApiKeyAuthorization:
      type: apiKey
      in: header
      name: Authorization
      description: 'Single header form (preferred): `Authorization: ApiKey key:secret` (use a `trk_live_` or `trk_test_` key).'
    ApiKeyId:
      type: apiKey
      in: header
      name: X-Api-Key
      description: 'Split-header form: the API key.'
    ApiKeySecret:
      type: apiKey
      in: header
      name: X-Api-Secret
      description: 'Split-header form: the API secret (sent with `X-Api-Key`).'

  parameters:
    AcceptLanguage:
      name: Accept-Language
      in: header
      required: false
      description: Response language. OPTIONAL — defaults to `en`. Only rejected if explicitly set to a value outside `en`, `es`, `fr`, `de`, `it`, `pt`.
      schema:
        type: string
        enum: [en, es, fr, de, it, pt]
        default: en

  headers:
    XRequestId:
      description: Unique id for the request; returned on error responses — quote it to Trekko support.
      schema: { type: string }
    RetryAfter:
      description: Seconds to wait before retrying (on 429).
      schema: { type: integer }

  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message, request_id]
          properties:
            code:
              type: string
              description: Stable, machine-readable error code (branch on this).
              enum:
                - rate_limited
                - invalid_api_key
                - forbidden
                - not_found
                - invalid_request
                - availability_expired
                - payment_required
                - booking_conflict
                - booking_failed
                - service_unavailable
                - internal_error
            message: { type: string }
            request_id: { type: string }

    SearchByType:
      type: integer
      description: '0 Accommodation · 1 City · 2 Area · 3 Landmark'
      enum: [0, 1, 2, 3]

    SearchResult:
      type: object
      properties:
        name: { type: string }
        sub_name: { type: string }
        search_by_id: { type: string }
        search_by_type: { $ref: '#/components/schemas/SearchByType' }
        latitude: { type: number }
        longitude: { type: number }
        landmarks: { type: array, items: { type: object } }
        area: { type: array, items: { type: object } }

    SearchResponse:
      type: object
      properties:
        data:
          type: array
          items: { $ref: '#/components/schemas/SearchResult' }

    Guest:
      type: object
      required: [id, age]
      properties:
        id: { type: integer, description: "Unique guest id (lead guest is id 1).", example: 1 }
        age: { type: integer, default: 30, example: 34 }

    GuestRoomDistribution:
      type: object
      required: [guest_ids]
      properties:
        guest_ids:
          type: array
          items: { type: integer }
          description: Guest ids assigned to this room. Every guest must be in exactly one room, and each room needs ≥1 adult (age ≥ 18).

    AvailabilityRequest:
      type: object
      required: [accommodation_id, start_date, end_date, guests, guests_room_distribution]
      properties:
        accommodation_id: { type: integer, example: 1376114 }
        start_date: { type: string, format: date, example: "2026-09-01" }
        end_date: { type: string, format: date, example: "2026-09-04" }
        guests:
          type: array
          items: { $ref: '#/components/schemas/Guest' }
        guests_room_distribution:
          type: array
          items: { $ref: '#/components/schemas/GuestRoomDistribution' }
        nationality: { type: string, description: "OPTIONAL. 2-letter ISO-3166-1 alpha-2 country code.", example: ES }
        currency_code: { type: string, description: "ISO-4217. Defaults to EUR.", example: EUR }

    CancellationPolicyRule:
      type: object
      description: |
        Vendor cancellation-ladder row as returned by availability/checkout and
        carried verbatim into confirm. Fields mirror the supplier payload
        (string-typed numbers).
      properties:
        From: { type: string, example: "0" }
        To: { type: string, example: "3" }
        DateFrom: { type: string, format: date, example: "2026-02-09" }
        DateFromHour: { type: string, example: "00:00" }
        DateTo: { type: string, format: date, example: "2026-02-13" }
        DateToHour: { type: string, example: "00:00" }
        Type: { type: string, example: "V" }
        FixedPrice: { type: string, example: "0" }
        PercentPrice: { type: string, example: "100" }
        Nights: { type: string, example: "0" }
        ApplicationTypeNights: { type: string, example: "Average" }

    AvailabilityOption:
      type: object
      properties:
        vendor: { type: string, example: "src_3f9a2b" }
        rate_plan_code: { type: string, example: RP-ABC-123 }
        booking_code: { type: string, example: BC-XYZ-789 }
        room_codes:
          type: [array, "null"]
          items: { type: string }
          description: Required for specific sources; null/absent otherwise.
        board_plan_text: { type: string, example: Room Only }
        nett_price: { type: number, example: 312.40 }
        minimum_price: { type: number, example: 330.00 }
        maximum_price: { type: number, example: 360.00 }
        currency_code: { type: string, example: EUR }
        currency_symbol: { type: string, example: "€" }
        cancellation_policy: { type: string }
        cancellation_policy_rules:
          type: array
          items: { $ref: '#/components/schemas/CancellationPolicyRule' }

    AvailabilityResponse:
      type: object
      properties:
        accommodation_id: { type: integer }
        name: { type: string }
        options:
          type: array
          items: { $ref: '#/components/schemas/AvailabilityOption' }

    CheckoutRequest:
      type: object
      required: [accommodation_id, vendor, rate_plan_code, start_date, end_date, guests, guests_room_distribution]
      properties:
        accommodation_id: { type: integer }
        vendor: { type: string }
        rate_plan_code: { type: string }
        start_date: { type: string, format: date }
        end_date: { type: string, format: date }
        guests:
          type: array
          items: { $ref: '#/components/schemas/Guest' }
        guests_room_distribution:
          type: array
          items: { $ref: '#/components/schemas/GuestRoomDistribution' }
        currency_code: { type: string, default: EUR }

    CheckoutResponse:
      type: object
      properties:
        vendor: { type: string, example: "src_3f9a2b" }
        booking_code: { type: string }
        room_codes:
          type: [array, "null"]
          items: { type: string }
        minimum_price: { type: number, example: 30.02 }
        maximum_price: { type: number, example: 33.19 }
        nett_price: { type: number, example: 31.6 }
        currency_code: { type: string, example: EUR }
        currency_symbol: { type: string, example: "€" }
        start_date: { type: string, format: date, example: "2026-02-12" }
        end_date: { type: string, format: date, example: "2026-02-13" }
        board_plan_text: { type: string, example: Room Only }
        cancellation_policy: { type: string }
        cancellation_policy_rules:
          type: array
          items: { $ref: '#/components/schemas/CancellationPolicyRule' }
        hotel_comments: { type: string }

    RoomData:
      type: object
      required: [room_name]
      properties:
        room_name: { type: string, minLength: 1, example: "Double Room" }
        bed_reference: { type: string, example: "1 double bed" }
        no_of_adults: { type: integer, example: 2 }
        no_of_children: { type: integer, example: 0 }
        guest_name: { type: string, description: "Optional.", example: "Ada Lovelace" }

    ConfirmGuest:
      type: object
      required: [id]
      properties:
        id: { type: integer }
        title: { type: string }
        name: { type: string }
        surname: { type: string }
        age: { type: integer, default: 30 }

    GuestsInformation:
      type: object
      description: Single object carrying the lead guest plus any extra occupants.
      required: [id, lead_guest_title, lead_guest_name, lead_guest_surname, lead_guest_nationality]
      properties:
        id: { type: integer, description: "Lead guest id; must be 1.", example: 1 }
        lead_guest_title: { type: string, example: Mr }
        lead_guest_name: { type: string, example: Ada }
        lead_guest_surname: { type: string, example: Lovelace }
        lead_guest_email: { type: string, format: email, description: Optional. }
        lead_guest_phone: { type: string, description: Optional. }
        lead_guest_nationality: { type: string, example: ES }
        lead_guest_age: { type: integer, default: 30, description: "Must be at least 18.", example: 34 }
        guests:
          type: array
          description: Additional occupants (optional).
          items: { $ref: '#/components/schemas/ConfirmGuest' }

    ConfirmBookingRequest:
      type: object
      required:
        - accommodation_id
        - vendor
        - booking_code
        - invoice_id
        - payment_type
        - minimum_price
        - maximum_price
        - nett_price
        - guests_information
        - guests_room_distribution
        - cancellation_policy
        - cancellation_policy_rules
        - board_plan_text
        - rooms_data
        - start_date
        - end_date
        - agencies_internal_booking_ref
        - base_price
        - base_currency
      properties:
        accommodation_id: { type: integer }
        vendor: { type: string, minLength: 1 }
        booking_code: { type: string }
        room_codes:
          type: array
          items: { type: string }
          description: Required when `vendor` is a specific source.
        invoice_id: { type: integer, description: From the payment step (e.g. pay_by_funds). }
        payment_type:
          type: string
          description: Must match the invoice. One of the long invoice-type strings.
          enum:
            - "Booking Payment by Own Card"
            - "Booking Payment by Funds"
            - "Booking Payment (Pay Later)"
            - "Booking Payment by Clients Card"
          example: "Booking Payment by Funds"
        minimum_price: { type: number }
        maximum_price: { type: number }
        nett_price: { type: number }
        base_price: { type: number }
        base_currency: { type: string, default: EUR }
        currency_code: { type: string, default: EUR }
        guests_information: { $ref: '#/components/schemas/GuestsInformation' }
        guests_room_distribution:
          type: array
          items: { $ref: '#/components/schemas/GuestRoomDistribution' }
        special_requests:
          type: object
          properties:
            arrival_time: { type: string }
            other_requests: { type: string }
        cancellation_policy: { type: string, minLength: 1 }
        cancellation_policy_rules:
          type: array
          items: { $ref: '#/components/schemas/CancellationPolicyRule' }
        board_plan_text: { type: string }
        rooms_data:
          type: array
          items: { $ref: '#/components/schemas/RoomData' }
        start_date: { type: string, format: date }
        end_date: { type: string, format: date }
        agencies_internal_booking_ref:
          type: string
          description: Your own reference, surfaced in the Trekko CRM. Use an `STK-` prefix.
        idempotency_key:
          type: string
          description: Optional stable per-attempt key; a duplicate submit returns the original booking. If omitted, the server derives one from invoice_id.

    ConfirmBookingResponse:
      type: object
      properties:
        booking_number: { type: integer, example: 1045 }
        booking_id: { type: string, example: TRK-1045, description: Stable id that survives SBPO rebooking. }

    BookingListItem:
      type: object
      description: Item shape from the live get_bookings output.
      properties:
        booking_id: { type: integer, example: 2 }
        is_rebooked: { type: boolean, example: false }
        old_booking_number: { type: [integer, "null"] }
        nights: { type: integer, example: 2 }
        rooms: { type: integer, example: 1 }
        check_in_date: { type: string, format: date, example: "2025-06-15" }
        check_out_date: { type: string, format: date, example: "2025-06-17" }
        cancellation_policy: { type: string }
        free_cancellation_until: { type: [string, "null"], format: date, example: "2025-06-12" }
        booking_date: { type: string, example: "2025-01-28 07:17:58" }
        is_paid: { type: boolean }
        payment_option: { type: string, example: "credit card" }
        price_wo_added_services: { type: number, example: 150.0 }
        client_price: { type: number, example: 125.0 }
        commission: { type: number, example: 25.0 }
        commission_percentage: { type: number, example: 25.0 }
        supplier_id: { type: integer }
        travel_agency_ref: { type: integer }
        agent_name: { type: string }
        guest_name: { type: string }
        hotel_name: { type: string, example: "Hotel Copacabana" }
        hotel_address: { type: string }
        hotel_country: { type: string, example: Spain }
        hotel_city: { type: string, example: "Palma de Mallorca" }
        cancellation_fee: { type: number, example: 0 }
        currency_code: { type: string, example: EUR }
        currency_symbol: { type: string, example: "€" }
        is_updated: { type: boolean }

    ListBookingsResponse:
      type: object
      properties:
        total_pages: { type: integer }
        current_page: { type: integer }
        items_per_page: { type: integer }
        data:
          type: array
          items: { $ref: '#/components/schemas/BookingListItem' }

    RebookingBlock:
      type: object
      description: Present when the booking was rebooked by SBPO to a cheaper rate.
      properties:
        rebooked: { type: boolean }
        times_rebooked: { type: integer }
        original_booking_number: { type: integer }
        gross_savings: { type: number }
        trekko_fee: { type: number }
        partner_savings: { type: number }
        credited_to: { type: string, enum: [virtual_wallet, credit_balance] }
        last_rebooked_at: { type: string, format: date-time }

    PartnerBooking:
      type: object
      properties:
        booking_id: { type: string, example: TRK-1045 }
        booking_number: { type: integer, example: 1045 }
        accommodation_name: { type: string }
        start_date: { type: string, format: date }
        end_date: { type: string, format: date }
        is_confirmed: { type: boolean }
        is_cancelled: { type: boolean }
        total_price: { type: number }
        currency_code: { type: string }
        agencies_internal_booking_ref: { type: [string, "null"] }
        rebooking:
          oneOf:
            - { $ref: '#/components/schemas/RebookingBlock' }
            - { type: 'null' }

    AccountBalance:
      type: object
      properties:
        currency_code: { type: string, example: EUR }
        funding_type: { type: string, enum: [prepaid, credit] }
        virtual_wallet: { type: [number, "null"], description: Present for prepaid accounts. }
        credit_balance: { type: [number, "null"], description: Present for credit accounts. }
        credit_limit: { type: [number, "null"] }

    SavingsLedger:
      type: object
      properties:
        current_page: { type: integer }
        total_pages: { type: integer }
        total_partner_savings: { type: number }
        total_trekko_fee: { type: number }
        currency_code: { type: string }
        data:
          type: array
          items:
            type: object
            properties:
              booking_id: { type: string }
              rebooked_at: { type: string, format: date-time }
              gross_savings: { type: number }
              trekko_fee: { type: number }
              partner_savings: { type: number }
              credited_to: { type: string }

  responses:
    Unauthorized:
      description: Missing/bad/inactive key or wrong secret.
      headers:
        X-Request-Id: { $ref: '#/components/headers/XRequestId' }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example: { error: { code: invalid_api_key, message: Invalid API key or secret., request_id: a1b2c3d4e5f6 } }
    Forbidden:
      description: Authenticated but not allowed.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example: { error: { code: forbidden, message: Permission denied., request_id: a1b2c3d4e5f6 } }
    BadRequest:
      description: Malformed body / failed validation.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example: { error: { code: invalid_request, message: Field 'start_date' is required., request_id: a1b2c3d4e5f6 } }
    NotFound:
      description: Resource not found.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example: { error: { code: not_found, message: Booking not found., request_id: a1b2c3d4e5f6 } }
    RateLimited:
      description: Per-key rate limit exceeded.
      headers:
        Retry-After: { $ref: '#/components/headers/RetryAfter' }
        X-Request-Id: { $ref: '#/components/headers/XRequestId' }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example: { error: { code: rate_limited, message: Request was throttled. Expected available in 30 seconds., request_id: a1b2c3d4e5f6 } }
    PaymentRequired:
      description: Payment not confirmed for the invoice (run the payment step first).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example: { error: { code: payment_required, message: Payment not confirmed for invoice 24881., request_id: a1b2c3d4e5f6 } }
    BookingConflict:
      description: Duplicate confirm / invoice already used (retry-safe — the original booking stands).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example: { error: { code: booking_conflict, message: A booking already exists for this idempotency key., request_id: a1b2c3d4e5f6 } }
    BookingFailed:
      description: Vendor-side failure; the charge was refunded — safe to retry.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example: { error: { code: booking_failed, message: Vendor rejected the booking; charge refunded., request_id: a1b2c3d4e5f6 } }
    ServiceUnavailable:
      description: Transient platform/vendor issue — retry with backoff.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example: { error: { code: service_unavailable, message: Service temporarily unavailable., request_id: a1b2c3d4e5f6 } }
