openapi: 3.1.0
info:
  title: Movmo API
  version: 1.0.0
  description: |
    Movmo's REST API for partner integrations. This spec covers the surface
    available to commercial partners (Aviare and others) who authenticate with an
    API Gateway API key plus a per-user Cognito ID token obtained via OAuth 2.0
    PKCE against a pre-registered client.

    A companion integration guide — code samples, the booking flow walkthrough,
    and provider coverage — will be published alongside this reference at
    `docs.movmo.io` (tracked in MOVMO-343).

    ### Authentication model

    Every protected route requires two credentials:

    1. `x-api-key` — API Gateway-issued partner key, provisioned out of band.
    2. `Authorization: Bearer <id-token>` — AWS Cognito ID token (RS256,
       JWKS-verified). Obtained by running the OAuth 2.0 PKCE flow against
       `auth.movmo.io`. Carries `custom:user_id`, `email`, `phone_number`,
       `jti`, `aud`, `exp`, and `sub` claims.

    Three flights-catalog routes intentionally run with the API key only and
    require no user token — these are the entry points that partners use to
    create an offer from their own provider search before the user has
    authenticated. Each is called out individually in this spec.

    ### Scopes (planned)

    Movmo will introduce per-operation OAuth scopes as part of
    [MOVMO-346](https://brianweber2.atlassian.net/browse/MOVMO-346). Until that
    ships, partner tokens carry the full set of user permissions delegated
    through `RBACMiddleware`. Operations in this spec are documented without
    scopes; once scopes ship, this spec will be revised to add per-operation
    scope requirements.
  contact:
    name: Brian Weber
    email: brian_weber@movmo.io
servers:
  - url: https://e2e.api.movmo.io
    description: Pre-production (e2e). The only environment exposed to partners today; the production base URL will be added when GA ships.
tags:
  - name: Authentication
    description: OAuth 2.0 PKCE token exchange and discovery metadata.
  - name: Profile
    description: User profile, contact-change flow, locale and travel preferences.
  - name: Passengers
    description: Saved passenger profiles and their identity documents.
  - name: Payments
    description: Payment method management. Stripe-backed today; a Spreedly migration is planned.
  - name: Flights
    description: Provider catalog, offer lifecycle, seat maps, and bookings.
security:
  - ApiKeyAuth: []
    BearerAuth: []
components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
      description: |
        API Gateway-issued partner key. Required on every request. Issued out
        of band during onboarding. Distinct from the per-user OAuth credentials.
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        AWS Cognito ID token (RS256, JWKS-verified). Obtained via the OAuth
        2.0 PKCE flow against `auth.movmo.io`. Carries `custom:user_id`,
        `email`, `phone_number`, `jti`, `aud`, `exp`, and `sub` claims.

  schemas:
    Error:
      type: object
      description: Standard error envelope returned by the monolith for 4xx and 5xx responses.
      properties:
        message:
          type: string
          description: Human-readable error summary. Always present.
          example: user payment method not found
        status_code:
          type: integer
          description: HTTP status code. Present on most flights and offer error paths; omitted on profile and passenger handlers.
          example: 404
        details:
          type: string
          description: Additional error context. Present on bindings-validation errors and provider-forwarded errors.
      required:
        - message

    ValidationError:
      type: object
      description: |
        Structured validation error returned by Gin's binding layer on invalid
        request bodies. Each field maps to the validator tag that failed.
      properties:
        message:
          type: string
          example: Validation failed
        fields:
          type: object
          additionalProperties:
            type: object
            properties:
              field:
                type: string
                example: new_value
              message:
                type: string
                example: "'new_value' failed on the 'required' tag"
          example:
            new_value:
              field: new_value
              message: "'new_value' failed on the 'required' tag"
      required:
        - message

    User:
      type: object
      description: |
        Registered Movmo user. `email` and `phone_number` are the only required
        fields; name fields, gender, date of birth, and KTN are optional.
        `email` and `phone_number` cannot be updated via `PUT /v1/users/{userid}`
        — clients must use the contact-change flow under `/v1/users/me/contact/*`.
      properties:
        id:
          type: string
          format: uuid
          description: Server-assigned user identifier.
          example: 00000000-0000-0000-0000-000000000001
        first_name:
          type: [string, "null"]
          description: Letters, spaces, hyphens, apostrophes only (2-100 chars).
          example: Jane
        middle_name:
          type: [string, "null"]
          example: Ada
        last_name:
          type: [string, "null"]
          example: Traveler
        suffix:
          type: [string, "null"]
          description: Alphabetic suffix (e.g., Jr, Sr, III). 1-10 chars.
          example: Jr
        email:
          type: string
          format: email
          maxLength: 255
          example: jane.traveler@example.com
        phone_number:
          type: string
          description: E.164 format, max 20 chars.
          example: "+15550142"
        date_of_birth:
          type: [string, "null"]
          format: date
          example: "1988-07-12"
        gender:
          type: [string, "null"]
          enum: [male, female]
          example: female
        known_traveler_number:
          type: [string, "null"]
          description: Optional TSA PreCheck / Global Entry KTN. Alphanumeric, 5-25 chars.
          example: "123456789"
        profile_photo_url:
          type: [string, "null"]
          format: uri
          example: https://cdn.movmo.io/u/jane.jpg
        last_login:
          type: string
          format: date-time
          readOnly: true
          example: "2026-04-18T14:22:11Z"
        created_at:
          type: string
          format: date-time
          readOnly: true
          example: "2024-09-01T10:12:45Z"
        updated_at:
          type: string
          format: date-time
          readOnly: true
          example: "2026-04-18T14:22:11Z"
      required:
        - email
        - phone_number

    UpdateUserRequest:
      type: object
      description: |
        Profile fields accepted by `PUT /v1/users/{userid}`. The `email` and
        `phone_number` fields on the `User` schema are deliberately omitted
        here — both are stripped server-side and must be changed via the
        contact-change flow under `/v1/users/me/contact/*`.
      properties:
        first_name:
          type: [string, "null"]
          description: Letters, spaces, hyphens, apostrophes only (2-100 chars).
          example: Jane
        middle_name:
          type: [string, "null"]
          example: Ada
        last_name:
          type: [string, "null"]
          example: Traveler
        suffix:
          type: [string, "null"]
          example: Jr
        date_of_birth:
          type: [string, "null"]
          format: date
          example: "1988-07-12"
        gender:
          type: [string, "null"]
          enum: [male, female]
          example: female
        known_traveler_number:
          type: [string, "null"]
          example: "123456789"
        profile_photo_url:
          type: [string, "null"]
          format: uri
          example: https://cdn.movmo.io/u/jane.jpg

    InitiateContactChangePayload:
      type: object
      description: Step 1 of the email/phone-change flow. Triggers an identity-verification OTP to the user's current email.
      properties:
        contact_type:
          type: string
          enum: [email, phone]
          example: email
        new_value:
          type: string
          description: The proposed new email address or E.164 phone number.
          example: jane.traveler+new@example.com
      required:
        - contact_type
        - new_value

    InitiateContactChangeResponse:
      type: object
      description: Session handle returned by Cognito for the subsequent verify-identity call.
      properties:
        session:
          type: string
          description: Opaque Cognito session token. Pass back in `VerifyIdentityPayload.session`.
          example: AYABeF...ZZ
        challenge_name:
          type: string
          description: Name of the Cognito challenge (typically `EMAIL_OTP`).
          example: EMAIL_OTP
      required:
        - session
        - challenge_name

    VerifyIdentityPayload:
      type: object
      description: |
        Step 2 of the email/phone-change flow. Verifies the identity OTP sent
        to the user's current email, then dispatches a separate verification
        code to the new contact. Email paths use Cognito's built-in verification;
        phone paths use AWS End User Messaging SMS with a DynamoDB-backed code.
      properties:
        code:
          type: string
          example: "423981"
        session:
          type: string
          description: Session token returned from the initiate step.
          example: AYABeF...ZZ
        contact_type:
          type: string
          enum: [email, phone]
          example: email
        new_value:
          type: string
          description: Must match the value submitted at the initiate step.
          example: jane.traveler+new@example.com
      required:
        - code
        - session
        - contact_type
        - new_value

    VerifyIdentityResponse:
      type: object
      description: |
        Access token returned in the response body rather than set as a cookie —
        keeps the Cookie header under API Gateway's 10 KB limit. Pass this token
        back in `ConfirmContactChangePayload.access_token`.
      properties:
        access_token:
          type: string
          example: eyJraWQi...lvAg
      required:
        - access_token

    ConfirmContactChangePayload:
      type: object
      description: |
        Step 3 of the email/phone-change flow. Verifies the code sent to the new
        contact, updates Cognito, and syncs the new value to Postgres.
      properties:
        code:
          type: string
          example: "817294"
        contact_type:
          type: string
          enum: [email, phone]
          example: email
        access_token:
          type: string
          description: The `access_token` returned from the verify-identity step.
          example: eyJraWQi...lvAg
        new_value:
          type: string
          description: Required for phone changes (DynamoDB lookup key). Optional for email.
          example: jane.traveler+new@example.com
      required:
        - code
        - contact_type
        - access_token

    LocalePreferences:
      type: object
      description: |
        User-level language, region, and currency preferences. Defaults to
        `en`/`US`/`USD` when the user has never set preferences.
      properties:
        user_id:
          type: string
          format: uuid
          example: 00000000-0000-0000-0000-000000000001
        language_code:
          type: string
          description: ISO 639-1 two-letter language code.
          example: en
        region_code:
          type: string
          description: ISO 3166-1 alpha-2 country code.
          example: US
        currency_code:
          type: string
          description: ISO 4217 three-letter currency code.
          example: USD
        updated_at:
          type: [string, "null"]
          format: date-time
          readOnly: true
      required:
        - user_id
        - language_code
        - region_code
        - currency_code

    UpsertLocalePreferencesRequest:
      type: object
      properties:
        language_code:
          type: string
          example: en
        region_code:
          type: string
          example: US
        currency_code:
          type: string
          example: USD
      required:
        - language_code
        - region_code
        - currency_code

    SeatPreferences:
      type: object
      properties:
        seat_type:
          type: string
          enum: [window, middle, aisle]
          example: window
        seating_zone:
          type: string
          enum: [front, middle, back]
          example: front
        extra_legroom:
          type: boolean
          example: true
        exit_row:
          type: boolean
          example: false

    BagPreferences:
      type: object
      properties:
        check_bag:
          type: boolean
          example: true

    MealPreferences:
      type: object
      properties:
        meal_types:
          type: array
          items:
            type: string
            enum:
              [
                standard,
                vegetarian,
                vegan,
                gluten_free,
                kosher,
                halal,
                diabetic,
              ]
          example: [vegetarian]
        dietary_notes:
          type: string
          example: No shellfish

    TravelPreferences:
      type: object
      description: |
        Seat, bag, meal, and special-assistance preferences used by provider
        booking flows to pre-fill ancillary selections.
      properties:
        user_id:
          type: string
          format: uuid
          example: 00000000-0000-0000-0000-000000000001
        seat:
          $ref: "#/components/schemas/SeatPreferences"
        bag:
          $ref: "#/components/schemas/BagPreferences"
        meal:
          $ref: "#/components/schemas/MealPreferences"
        special_assistance:
          type: array
          items:
            type: string
            enum:
              - wheelchair_assistance
              - visual_impairment
              - hearing_impairment
              - service_animal
              - infant_or_child
          example: []
        updated_at:
          type: [string, "null"]
          format: date-time
          readOnly: true
      required:
        - user_id
        - seat
        - bag
        - meal
        - special_assistance

    UpsertTravelPreferencesRequest:
      type: object
      properties:
        seat:
          $ref: "#/components/schemas/SeatPreferences"
        bag:
          $ref: "#/components/schemas/BagPreferences"
        meal:
          $ref: "#/components/schemas/MealPreferences"
        special_assistance:
          type: array
          items:
            type: string
            enum:
              - wheelchair_assistance
              - visual_impairment
              - hearing_impairment
              - service_animal
              - infant_or_child
      required:
        - seat
        - bag
        - meal
        - special_assistance

    Passenger:
      type: object
      description: |
        Saved passenger profile owned by a user. `traveler_type` is optional on
        write; when omitted the server derives it from `date_of_birth` and the
        current date (or from the `departure_date` query parameter on list).
        `known_traveler_number` is **read-only** — it is joined at query time
        from the passenger's identity-documents records and is null when the
        passenger has no KTN on file. Deletes are soft — rows are retained with
        a `deleted_at` timestamp; foreign keys from booking-passenger rows are
        `RESTRICT` to preserve booking history.
      properties:
        id:
          type: string
          format: uuid
          example: 11111111-1111-1111-1111-111111111111
        user_id:
          type: string
          format: uuid
          readOnly: true
          description: Set from the URL path on write; never accepted in the request body.
          example: 00000000-0000-0000-0000-000000000001
        first_name:
          type: string
          description: Letters, spaces, hyphens, apostrophes only (2-100 chars).
          example: Marco
        middle_name:
          type: string
          example: Luiz
        last_name:
          type: string
          example: Aviator
        gender:
          type: string
          enum: [male, female]
          example: male
        date_of_birth:
          type: string
          format: date
          example: "1990-05-20"
        email:
          type: string
          format: email
          example: marco.aviator@example.com
        phone_number:
          type: string
          description: E.164 format, max 20 chars.
          example: "+15550198"
        nationality:
          type: string
          description: ISO 3166-1 alpha-2 country code.
          example: US
        traveler_type:
          type: string
          enum: [adult, child, infant]
          example: adult
        is_default:
          type: boolean
          example: true
        known_traveler_number:
          type: [string, "null"]
          readOnly: true
          description: Read-only. Joined from the passenger's primary identity document at query time.
          example: "987654321"
        created_at:
          type: string
          format: date-time
          readOnly: true
        updated_at:
          type: string
          format: date-time
          readOnly: true
      required:
        - first_name
        - last_name
        - gender
        - date_of_birth

    IdentityDocument:
      type: object
      description: |
        Identity document attached to a saved passenger. Document numbers are
        encrypted at rest with a KMS-managed key.
      properties:
        id:
          type: string
          format: uuid
          example: 22222222-2222-2222-2222-222222222222
        user_passenger_id:
          type: string
          format: uuid
          readOnly: true
          example: 11111111-1111-1111-1111-111111111111
        document_type:
          type: string
          enum:
            - passport
            - visa
            - drivers_license
            - national_id
            - known_traveler_number
            - redress_number
            - tax_id
          example: passport
        document_number:
          type: string
          description: 3-50 characters. Stored encrypted.
          example: A12345678
        issuing_country_code:
          type: string
          description: ISO 3166-1 alpha-2 country code.
          example: US
        issuing_authority:
          type: string
          example: U.S. Department of State
        issued_date:
          type: [string, "null"]
          format: date
          example: "2020-03-15"
        expires_on:
          type: [string, "null"]
          format: date
          description: Required for document types that expire (passport, visa, drivers_license).
          example: "2030-03-14"
        nationality_on_document:
          type: string
          description: ISO 3166-1 alpha-2 country code.
          example: US
        birth_country:
          type: string
          description: ISO 3166-1 alpha-2 country code.
          example: US
        is_primary:
          type: boolean
          description: Primary document for this passenger. Promotes `known_traveler_number` into the parent passenger record when `document_type` is `known_traveler_number`.
          example: true
        created_at:
          type: string
          format: date-time
          readOnly: true
        updated_at:
          type: string
          format: date-time
          readOnly: true
      required:
        - document_type
        - document_number
        - issuing_country_code

    PaymentMethod:
      type: object
      description: |
        Stored payment method. Only the last 4 digits, brand, expiration, and
        default flag are held in Movmo's database; the full PAN never touches
        Movmo infrastructure — tokenization happens client-side via Stripe
        Elements, and `paymentMethodId` / `customerId` / `token` are Stripe
        references.
      properties:
        id:
          type: string
          format: uuid
          example: 33333333-3333-3333-3333-333333333333
        userId:
          type: string
          format: uuid
          readOnly: true
          example: 00000000-0000-0000-0000-000000000001
        customerId:
          type: string
          description: Stripe customer ID.
          example: cus_TestAviarePartner0001
        paymentMethodId:
          type: string
          description: Stripe payment method ID.
          example: pm_TestVisa41111111111234
        methodType:
          type: string
          enum: [credit_card, paypal, googlepay, klarna]
          example: credit_card
        token:
          type: string
          description: Stripe client token. Write-only from the partner perspective.
          example: tok_Test411111...
        last4:
          type: string
          description: Exactly four digits.
          example: "1234"
        brand:
          type: string
          enum: [amex, visa, mastercard, discover]
          example: visa
        expMonth:
          type: integer
          minimum: 1
          maximum: 12
          example: 12
        expYear:
          type: integer
          minimum: 2024
          maximum: 2050
          example: 2030
        isDefault:
          type: boolean
          example: true
        createdAt:
          type: string
          format: date-time
          readOnly: true
        updatedAt:
          type: string
          format: date-time
          readOnly: true
      required:
        - customerId
        - paymentMethodId
        - methodType
        - token
        - last4
        - brand
        - expMonth
        - expYear

    PaymentMethodEdit:
      type: object
      description: |
        Narrow edit payload for `PUT /v1/users/{userid}/payment-methods/{methodid}`.
        Only expiration month/year and the default flag can be updated — re-keying
        a card requires creating a new payment method.
      properties:
        expMonth:
          type: integer
          minimum: 1
          maximum: 12
          example: 11
        expYear:
          type: integer
          minimum: 2024
          maximum: 2050
          example: 2031
        isDefault:
          type: boolean
          example: false

    CreatedIdResponse:
      type: object
      description: Canonical 201 response body used by POST endpoints that create a single resource identified by UUID.
      properties:
        id:
          type: string
          format: uuid
          example: 44444444-4444-4444-4444-444444444444
      required:
        - id

    Provider:
      type: object
      description: Flight provider metadata. `config` carries provider-specific configuration and varies per provider.
      properties:
        id:
          type: string
          enum: [amadeus, duffel, dohop, darwin]
          example: darwin
        name:
          type: string
          example: Darwin
        type:
          type: string
          enum: [GDS, NDC, API, OTA]
          example: API
        capabilities:
          $ref: "#/components/schemas/ProviderCapabilities"
        config:
          type: object
          additionalProperties: true
          description: Provider-specific configuration. Shape varies by provider.
        is_active:
          type: boolean
          example: true
        priority:
          type: integer
          description: Search priority. Lower is higher priority.
          example: 1
      required:
        - id
        - name
        - type
        - capabilities
        - is_active

    ProviderCapabilities:
      type: object
      description: |
        Feature and limit capabilities advertised by a provider. Partners query
        this before rendering booking UI to decide which steps (separate pricing,
        seat selection, hold booking) apply.
      properties:
        supports_direct_booking:
          type: boolean
          example: true
        requires_pricing:
          type: boolean
          example: false
        requires_payment_step:
          type: boolean
          example: true
        requires_order_creation:
          type: boolean
          example: false
        supports_offer_passenger_update:
          type: boolean
          example: true
        supports_integrated_pricing_booking:
          type: boolean
          example: false
        supports_seat_selection:
          type: boolean
          example: true
        supports_bag_selection:
          type: boolean
          example: false
        supports_refunds:
          type: boolean
          example: false
        supports_changes:
          type: boolean
          example: false
        supports_partial_booking:
          type: boolean
          example: false
        supports_hold_booking:
          type: boolean
          example: true
        max_passengers:
          type: integer
          example: 9
        max_connections:
          type: integer
          example: 4
        supported_currencies:
          type: array
          items:
            type: string
          example: [USD, EUR]
        supported_cabin_classes:
          type: array
          items:
            type: string
          example: [economy, premium_economy, business]
        max_advance_booking_days:
          type: integer
          example: 365
        min_advance_booking_hours:
          type: integer
          example: 4
        search_timeout_seconds:
          type: integer
          example: 30
        pricing_timeout_seconds:
          type: integer
          example: 15
        booking_timeout_seconds:
          type: integer
          example: 60

    ProviderOfferInput:
      type: object
      description: |
        Provider-native offer payload used for `POST .../offers` and
        `PUT .../offers/{offer_id}`. The shape is provider-specific — partners
        pass the raw offer struct returned by their own provider search verbatim.
        For Darwin this is the availability-response object from
        `/perform-availability`; for Amadeus it is a flight-offer object.
      additionalProperties: true

    Offer:
      type: object
      description: |
        Normalized flight offer returned by Movmo's catalog layer. Aggregates
        provider offer data into a consistent shape with slices, segments,
        passengers, fare metadata, and conditions. Fields marked
        `additionalProperties: true` retain provider-native data; use them when
        you need raw provider semantics.
      properties:
        id:
          type: string
          example: off_darwin_5iA9kC
        provider_id:
          type: string
          enum: [amadeus, duffel, dohop, darwin]
          example: darwin
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        expires_at:
          type: string
          format: date-time
          description: Offer expiration. Partners must create a booking before this timestamp.
          example: "2026-09-01T12:00:00Z"
        total_amount:
          type: string
          description: Decimal string, currency-minor-unit-preserving.
          example: "1245.00"
        total_currency:
          type: string
          example: USD
        base_amount:
          type: string
          example: "1099.00"
        base_currency:
          type: string
          example: USD
        tax_amount:
          type: [string, "null"]
          example: "146.00"
        tax_currency:
          type: [string, "null"]
          example: USD
        total_emissions_kg:
          type: [string, "null"]
        passenger_identity_documents_required:
          type: boolean
        supported_passenger_identity_document_types:
          type: array
          items:
            type: string
        supported_loyalty_programmes:
          type: array
          items:
            type: string
          description: Airline IATA codes whose loyalty programmes can be attached to a passenger on this offer.
          example: [DL, AA, UA]
        owner:
          type: object
          description: Marketing carrier metadata.
          additionalProperties: true
        passengers:
          type: array
          items:
            type: object
            additionalProperties: true
        slices:
          type: array
          items:
            type: object
            additionalProperties: true
          description: Journey slices. Each slice has segments, origin/destination, and conditions.
        conditions:
          type: object
          additionalProperties: true
        payment_requirements:
          type: object
          additionalProperties: true
        private_fares:
          type: array
          description: Private/corporate fare entries applied to the offer. Omitted when no private fares apply.
          items:
            type: object
            properties:
              corporate_code:
                type: string
              tour_code:
                type: string
              tracking_reference:
                type: string
        available_services:
          type: array
          description: Ancillaries available for purchase against the offer (bags, seats, meals, etc.). Omitted when none are offered.
          items:
            type: object
            properties:
              id:
                type: string
              type:
                type: string
                description: Service category (e.g., `baggage`, `seat`, `meal`, `cancel_for_any_reason`).
              name:
                type: string
              description:
                type: string
              total_amount:
                type: string
                description: Decimal string.
              total_currency:
                type: string
              segment_ids:
                type: array
                items:
                  type: string
              passenger_ids:
                type: array
                items:
                  type: string
              maximum_quantity:
                type: integer
              weight:
                type: number
                format: double
              weight_unit:
                type: string
              dimensions:
                type: object
                properties:
                  length:
                    type: integer
                  width:
                    type: integer
                  height:
                    type: integer
            required:
              - id
              - type
              - total_amount
              - total_currency
        fare_class_metadata:
          type: [object, "null"]
          description: |
            Fare-class characteristics used for UI display (seat selection,
            baggage allowance, change/refund policy, boarding priority).
            Omitted when the provider does not surface a per-fare-class
            metadata block.
          properties:
            seat_selection_allowed:
              type: boolean
            seat_selection_fee:
              type: [string, "null"]
              description: Decimal string. `null` or `"0"` means free.
            seat_selection_currency:
              type: [string, "null"]
            extra_legroom:
              type: boolean
            carry_on_bags:
              type: integer
            checked_bags:
              type: integer
            checked_bags_paid:
              type: boolean
            changes_allowed:
              type: boolean
            change_penalty_amount:
              type: [string, "null"]
            change_penalty_currency:
              type: [string, "null"]
            refunds_allowed:
              type: boolean
            refund_penalty_amount:
              type: [string, "null"]
            refund_penalty_currency:
              type: [string, "null"]
            boarding_priority:
              type: string
              enum: [last, standard, early, priority]
        raw_offer:
          description: Provider-native offer data. Present when the provider retains raw form.
          additionalProperties: true
      required:
        - id
        - provider_id
        - total_amount
        - total_currency
        - slices

    CreateOfferResponse:
      type: object
      properties:
        offer_id:
          type: string
          example: off_darwin_5iA9kC
      required:
        - offer_id

    OfferPassengerMapping:
      type: object
      description: Association between a saved passenger and a provider-assigned passenger ID for an offer.
      properties:
        user_passenger_id:
          type: string
          format: uuid
          example: 11111111-1111-1111-1111-111111111111
        provider_passenger_id:
          type: string
          description: Provider-assigned passenger identifier from the offer payload.
          example: pas_dar_00009hj8USM7Ncg31cBCL
      required:
        - provider_passenger_id

    PassengerUpdateRequest:
      type: object
      description: |
        Fields updatable on an offer passenger. Empty fields are ignored.
        Used primarily for adding KTN, loyalty accounts, and identity documents
        after an offer has been created.
      properties:
        passenger_id:
          type: string
          description: Provider passenger ID. Required by some providers (Amadeus).
          example: pas_dar_00009hj8USM7Ncg31cBCL
        first_name:
          type: string
          example: Marco
        middle_name:
          type: string
          example: Luiz
        last_name:
          type: string
          example: Aviator
        date_of_birth:
          type: string
          format: date
          example: "1990-05-20"
        gender:
          type: string
          enum: [male, female]
          example: male
        known_traveler_number:
          type: string
          example: "987654321"
        loyalty_programme_accounts:
          type: array
          items:
            type: object
            properties:
              airline_iata_code:
                type: string
                example: DL
              account_number:
                type: string
                example: "9000000001"
            required:
              - airline_iata_code
              - account_number
        identity_documents:
          type: array
          items:
            type: object
            properties:
              expires_on:
                type: string
                format: date
                example: "2030-03-14"
              issuing_country_code:
                type: string
                example: US
              type:
                type: string
                enum:
                  [
                    passport,
                    tax_id,
                    known_traveler_number,
                    passenger_redress_number,
                  ]
                example: passport
              unique_identifier:
                type: string
                example: A12345678
            required:
              - expires_on
              - issuing_country_code
              - type
              - unique_identifier

    SeatMap:
      type: object
      description: Normalized seat map for a single offer. Returned as an array — one entry per segment that supports seat selection.
      properties:
        offer_id:
          type: string
          example: off_darwin_5iA9kC
        segment_id:
          type: string
          example: seg_darwin_LAXJFK_001
        seat_map:
          type: object
          additionalProperties:
            $ref: "#/components/schemas/SeatInfo"
        pricing:
          type: object
          additionalProperties:
            $ref: "#/components/schemas/SeatPrice"

    SeatInfo:
      type: object
      properties:
        seat_number:
          type: string
          example: 14A
        row:
          type: string
          example: "14"
        column:
          type: string
          example: A
        is_available:
          type: boolean
          example: true
        is_occupied:
          type: boolean
          example: false
        is_blocked:
          type: boolean
          example: false
        seat_type:
          type: string
          example: exit_row
        disclosures:
          type: array
          items:
            type: string

    SeatPrice:
      type: object
      properties:
        amount:
          type: number
          format: double
          example: 49.99
        currency:
          type: string
          example: USD

    SeatSelection:
      type: object
      properties:
        segment_id:
          type: string
          example: seg_darwin_LAXJFK_001
        passenger_id:
          type: string
          example: pas_dar_00009hj8USM7Ncg31cBCL
        seat_number:
          type: string
          example: 14A
        row:
          type: string
          example: "14"
        column:
          type: string
          example: A
        price:
          type: number
          format: double
          example: 49.99
        currency:
          type: string
          example: USD
      required:
        - segment_id
        - passenger_id
        - seat_number

    ContactInfo:
      type: object
      properties:
        email:
          type: string
          format: email
          example: jane.traveler@example.com
        phone_number:
          type: string
          example: "+15550142"
        first_name:
          type: string
          example: Jane
        last_name:
          type: string
          example: Traveler
        title:
          type: string
          example: Ms
      required:
        - email
        - phone_number
        - first_name
        - last_name

    BookingPassengerInput:
      type: object
      description: |
        Passenger details submitted on `POST .../bookings`. Shape mirrors the
        normalized `MovmoPassenger` used internally by the flight service.
      properties:
        id:
          type: string
          description: Database passenger ID (may be empty on creation; Duffel doesn't require it).
          example: 11111111-1111-1111-1111-111111111111
        provider_passenger_id:
          type: string
          example: pas_dar_00009hj8USM7Ncg31cBCL
        type:
          type: string
          example: adult
        first_name:
          type: string
          example: Jane
        middle_name:
          type: string
          example: Ada
        last_name:
          type: string
          example: Traveler
        born_on:
          type: string
          format: date
          example: "1988-07-12"
        email:
          type: string
          format: email
          example: jane.traveler@example.com
        phone_number:
          type: string
          example: "+15550142"
        title:
          type: string
          example: Ms
        gender:
          type: string
          enum: [male, female]
          example: female
        known_traveler_number:
          type: string
          example: "123456789"
        identity_documents:
          type: array
          items:
            type: object
            properties:
              expires_on:
                type: string
                format: date
              issuing_country_code:
                type: string
              type:
                type: string
                enum:
                  [
                    passport,
                    tax_id,
                    known_traveler_number,
                    passenger_redress_number,
                  ]
              unique_identifier:
                type: string
        traveler_type:
          type: string
          enum: [adult, child, infant]
          example: adult

    CreateBookingRequest:
      type: object
      description: |
        Complete booking request including provider, offer, passenger, contact,
        and payment details. Partners pass the Stripe payment method ID and
        customer ID that were created client-side; Movmo creates and confirms a
        payment intent server-side and then dispatches the booking to the
        provider. `idempotency_key` is required — repeat calls with the same key
        return the existing confirmed booking rather than double-booking.
      properties:
        provider_id:
          type: string
          readOnly: true
          description: Taken from the `{provider}` path parameter; ignored if present in the request body.
          example: darwin
        offer_id:
          type: string
          example: off_darwin_5iA9kC
        passengers:
          type: array
          items:
            $ref: "#/components/schemas/BookingPassengerInput"
        contact_info:
          $ref: "#/components/schemas/ContactInfo"
        payment_method_id:
          type: string
          format: uuid
          description: Movmo payment method UUID, not the Stripe `pm_` ID.
          example: 33333333-3333-3333-3333-333333333333
        customer_id:
          type: string
          description: Stripe customer ID associated with the user.
          example: cus_TestAviarePartner0001
        currency:
          type: string
          example: USD
        user_id:
          type: string
          format: uuid
          example: 00000000-0000-0000-0000-000000000001
        idempotency_key:
          type: string
          description: Required. Stable key per booking attempt; duplicates return the existing booking.
          example: 55555555-5555-5555-5555-555555555555
        metadata:
          type: object
          additionalProperties:
            type: string
        services:
          type: array
          description: Optional ancillary service selections (Duffel-specific).
          items:
            type: object
            properties:
              id:
                type: string
              quantity:
                type: integer
            required:
              - id
              - quantity
        order_type:
          type: string
          description: Optional. `instant` (default) or `hold`.
          enum: [instant, hold]
        seat_selections:
          type: array
          items:
            $ref: "#/components/schemas/SeatSelection"
      required:
        - offer_id
        - passengers
        - contact_info
        - payment_method_id
        - customer_id
        - currency
        - user_id
        - idempotency_key

    RetryBookingRequest:
      allOf:
        - $ref: "#/components/schemas/CreateBookingRequest"
        - type: object
          description: |
            Retry a booking after a price-change error. `order_id` comes from the
            `PriceChangeError` returned by the original booking attempt. Used
            primarily with Dohop's two-phase book flow.
          properties:
            order_id:
              type: string
              description: Provider order ID from the failed booking's price-change error.
              example: ord_dohop_9b3L2x
          required:
            - order_id

    BookingResponse:
      type: object
      properties:
        booking:
          $ref: "#/components/schemas/Booking"
        payment_intent_id:
          type: string
          example: pi_Test1Mj8L00MovmoBookingVisa
        status:
          type: string
          enum:
            - pending
            - payment_authorized
            - booking_created
            - payment_captured
            - failed
            - cancelled
            - confirmed
          example: confirmed
        metadata:
          type: object
          additionalProperties:
            type: string

    Booking:
      type: object
      description: Persisted booking record. Denormalized trip-summary fields are populated at booking time for fast list queries.
      properties:
        id:
          type: string
          format: uuid
          example: 66666666-6666-6666-6666-666666666666
        flight_offer_id:
          type: [string, "null"]
          description: Nullable for providers that don't persist offers locally.
          example: off_darwin_5iA9kC
        user_id:
          type: string
          format: uuid
          example: 00000000-0000-0000-0000-000000000001
        payment_method_id:
          type: string
          format: uuid
          example: 33333333-3333-3333-3333-333333333333
        airline_pnr:
          type: string
          example: AVRA1B
        payment_intent_id:
          type: string
          example: pi_Test1Mj8L00MovmoBookingVisa
        total_amount:
          type: number
          format: double
          example: 1245.00
        currency:
          type: string
          example: USD
        status:
          type: string
          enum:
            - pending
            - payment_authorized
            - booking_created
            - payment_captured
            - failed
            - cancelled
            - confirmed
          example: confirmed
        idempotency_key:
          type: string
          example: 55555555-5555-5555-5555-555555555555
        provider_id:
          type: string
          enum: [amadeus, duffel, dohop, darwin]
          example: darwin
        provider_offer_id:
          type: string
          description: Provider's native offer ID.
          example: off_dar_5iA9kC
        provider_order_id:
          type: string
          description: Provider's order / booking ID.
          example: ord_dar_77xP1z
        created_at:
          type: string
          format: date-time
          readOnly: true
        updated_at:
          type: string
          format: date-time
          readOnly: true
        trip_type:
          type: string
          example: round_trip
        origin_city:
          type: string
          example: Los Angeles
        origin_iata_code:
          type: string
          example: LAX
        destination_city:
          type: string
          example: New York
        destination_iata_code:
          type: string
          example: JFK
        departure_date:
          type: [string, "null"]
          format: date-time
        return_date:
          type: [string, "null"]
          format: date-time
        airline_name:
          type: string
          example: Darwin Air
        airline_iata_code:
          type: string
          example: DA
        airline_logo_url:
          type: string
          format: uri
        passenger_count:
          type: integer
          example: 2
        adult_count:
          type: integer
          example: 2
        child_count:
          type: integer
          example: 0
        infant_count:
          type: integer
          example: 0
      required:
        - id
        - user_id
        - provider_id
        - status
        - idempotency_key

    BookingPassenger:
      type: object
      description: Booking-passenger association enriched with the saved passenger record plus ticketing info.
      allOf:
        - $ref: "#/components/schemas/Passenger"
        - type: object
          properties:
            ticket_number:
              type: string
              example: "0142345678901"
            seat_number:
              type: string
              example: 14A
            fare_basis_code:
              type: string
              example: YOWAV
            seat_selections:
              type: array
              items:
                type: object
                properties:
                  id:
                    type: string
                    format: uuid
                  booking_id:
                    type: string
                    format: uuid
                  user_passenger_id:
                    type: string
                    format: uuid
                  segment_id:
                    type: string
                  seat_number:
                    type: string
                  seat_row:
                    type: string
                  seat_column:
                    type: string
                  price:
                    type: [number, "null"]
                    format: double
                  currency:
                    type: string
                  created_at:
                    type: string
                    format: date-time
                    readOnly: true
                  updated_at:
                    type: string
                    format: date-time
                    readOnly: true

    CancellationResult:
      type: object
      properties:
        booking_id:
          type: string
          format: uuid
          example: 66666666-6666-6666-6666-666666666666
        is_cancelled:
          type: boolean
          example: true
        refund_amount:
          type: [number, "null"]
          format: double
          example: 1150.00
        refund_currency:
          type: [string, "null"]
          example: USD
        cancelled_at:
          type: string
          format: date-time
        reason:
          type: string
      required:
        - booking_id
        - is_cancelled
        - cancelled_at

    TripSummary:
      type: object
      description: |
        Lightweight trip summary returned by `GET /v1/users/{userid}/bookings`.
        Populated from denormalized columns on `provider_flight_bookings` plus
        a computed `status` (`upcoming` / `completed` / `cancelled`) derived
        from departure date and the underlying booking status. Distinct from
        `Booking` — this shape omits payment, offer, and idempotency fields and
        is intended for travel-history list views only. Use `GET
        /v1/users/{userid}/bookings/{bookingid}` for the full booking record.
      properties:
        id:
          type: string
          format: uuid
          example: 66666666-6666-6666-6666-666666666666
        user_id:
          type: string
          format: uuid
          example: 00000000-0000-0000-0000-000000000001
        status:
          type: string
          description: Computed status. Not the underlying provider booking status.
          enum: [upcoming, completed, cancelled]
          example: upcoming
        trip_type:
          type: string
          example: round_trip
        origin_city:
          type: string
          example: Los Angeles
        origin_iata_code:
          type: string
          example: LAX
        destination_city:
          type: string
          example: New York
        destination_iata_code:
          type: string
          example: JFK
        departure_date:
          type: [string, "null"]
          format: date
          example: "2026-09-01"
        return_date:
          type: [string, "null"]
          format: date
          example: "2026-09-08"
        airline_name:
          type: string
          example: American Airlines
        airline_iata_code:
          type: string
          example: AA
        airline_logo_url:
          type: [string, "null"]
          format: uri
          example: https://cdn.movmo.io/airlines/aa.svg
        passenger_count:
          type: integer
          example: 2
        adult_count:
          type: integer
          example: 2
        child_count:
          type: integer
          example: 0
        infant_count:
          type: integer
          example: 0
        pnr:
          type: string
          description: Airline record locator. May be empty when the booking is still pending ticketing.
          example: ABC123
        total_price:
          type: number
          format: double
          example: 1245.00
        currency:
          type: string
          example: USD
        created_at:
          type: string
          format: date-time
          readOnly: true
        updated_at:
          type: string
          format: date-time
          readOnly: true
      required:
        - id
        - user_id
        - status
        - trip_type
        - origin_city
        - origin_iata_code
        - destination_city
        - destination_iata_code
        - airline_name
        - airline_iata_code
        - passenger_count
        - adult_count
        - child_count
        - infant_count
        - pnr
        - total_price
        - currency
        - created_at
        - updated_at

    OAuthTokenRequest:
      type: object
      description: |
        Token endpoint request body. Accepts both `application/json` and
        `application/x-www-form-urlencoded` (RFC 6749 §4.1.3). `grant_type`
        selects the flow:

        - `authorization_code`: requires `code`, `code_verifier`, `redirect_uri`,
          and `client_id`. Public clients omit `client_secret`; confidential
          partner clients include it.
        - `refresh_token`: requires `refresh_token` and `client_id`.
      properties:
        grant_type:
          type: string
          enum: [authorization_code, refresh_token]
          example: authorization_code
        code:
          type: string
          description: Authorization code returned by the auth-ui redirect. Required for `authorization_code`.
          example: Rkm8b8Z5wV4-bwGqXh0pf3Cx7yl4T8TlrzaUeY_vBLM
        code_verifier:
          type: string
          description: PKCE verifier matching the `code_challenge` sent on authorization. Required for `authorization_code`.
          example: wWw9RT4zFOSK6rS6rnK5Bq5bQ5dXp4d5y5f5j5Q5dXp
        client_id:
          type: string
          description: Partner's pre-registered client identifier (e.g., `aviare-prod`).
          example: aviare-prod
        redirect_uri:
          type: string
          format: uri
          description: Must match the `redirect_uri` sent on authorization. Required for `authorization_code`.
          example: https://partner.example.com/oauth/callback
        refresh_token:
          type: string
          description: Required for `refresh_token` grant.
          example: eyJjdHkiOi...refresh
        client_secret:
          type: string
          description: Required for confidential clients on either grant.
          example: partner_secret_redacted
        resource:
          type: string
          format: uri
          description: Optional RFC 8707 resource indicator. Must match the value sent on authorization if present.
          example: https://e2e.api.movmo.io
      required:
        - grant_type
        - client_id

    OAuthTokenResponse:
      type: object
      properties:
        access_token:
          type: string
          description: "Cognito ID token (RS256). Pass as `Authorization: Bearer <access_token>` on subsequent calls."
          example: eyJraWQi...accesstoken
        refresh_token:
          type: string
          description: Cognito refresh token. Present on `authorization_code`; omitted on `refresh_token` grant when Cognito does not rotate.
          example: eyJjdHkiOi...refresh
        token_type:
          type: string
          example: Bearer
        expires_in:
          type: integer
          description: |
            Seconds until `access_token` expiration. The value depends on the
            grant:

            - `authorization_code`: 900 seconds (15 minutes), aligned with
              Movmo's session-token TTL.
            - `refresh_token`: pass-through from Cognito (typically 3600
              seconds / 1 hour).

            Treat as authoritative — always honour this field rather than
            assuming a fixed lifetime.
          example: 900
      required:
        - access_token
        - token_type
        - expires_in

    OAuthErrorResponse:
      type: object
      description: |
        RFC 6749 §5.2 token-endpoint error envelope. Returned by
        `POST /v1/oauth/token` and `POST /v1/oauth/register` instead of the
        generic `Error` shape so OAuth client libraries can dispatch on
        `error` directly.
      properties:
        error:
          type: string
          description: |
            Stable machine-readable error code. RFC 6749 codes
            (`invalid_request`, `invalid_client`, `invalid_grant`,
            `unauthorized_client`, `unsupported_grant_type`,
            `invalid_scope`) plus Movmo-specific codes (`server_error`,
            `invalid_target` for RFC 8707 resource mismatch,
            `invalid_redirect_uri`, `invalid_client_metadata`,
            `missing_session`, `missing_refresh_token`).
          example: invalid_grant
        error_description:
          type: string
          description: Optional human-readable detail. Movmo also returns this as `detail` on bind/validation failures for backwards compatibility.
          example: authorization code has expired
        error_uri:
          type: string
          format: uri
          description: Optional pointer to documentation about the error.
        detail:
          type: string
          description: Legacy alias used on bind/validation failures. Prefer `error_description` going forward.
      required:
        - error

    OAuthDiscoveryMetadata:
      type: object
      description: |
        RFC 8414 authorization-server metadata. Also served at
        `/.well-known/openid-configuration` for OIDC discovery.
      properties:
        issuer:
          type: string
          format: uri
          example: https://e2e.api.movmo.io
        authorization_endpoint:
          type: string
          format: uri
          example: https://auth.e2e.movmo.io
        token_endpoint:
          type: string
          format: uri
          example: https://e2e.api.movmo.io/v1/oauth/token
        registration_endpoint:
          type: string
          format: uri
          example: https://e2e.api.movmo.io/v1/oauth/register
        response_types_supported:
          type: array
          items:
            type: string
          example: [code]
        grant_types_supported:
          type: array
          items:
            type: string
          example: [authorization_code, refresh_token]
        token_endpoint_auth_methods_supported:
          type: array
          items:
            type: string
          example: [none, client_secret_basic]
        code_challenge_methods_supported:
          type: array
          items:
            type: string
          example: [S256]
        scopes_supported:
          type: array
          items:
            type: string
          description: OIDC identity scopes advertised today. Movmo permission scopes (MOVMO-346) will be added when scope enforcement ships.
          example: [openid, email, profile]
      required:
        - issuer
        - token_endpoint
        - response_types_supported
        - grant_types_supported

  responses:
    Unauthorized:
      description: Missing, malformed, or expired bearer token.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            message: unauthorized
    Forbidden:
      description: Authenticated but not allowed to access this resource. Typically a user attempting to touch another user's record.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            message: not allowed
    NotFound:
      description: The resource does not exist or has been soft-deleted.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            message: user passenger not found
    Conflict:
      description: The resource already exists or conflicts with an existing record (e.g., email already in use, duplicate identity document).
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            message: email already in use
    BadRequest:
      description: Malformed request body or failed validation.
      content:
        application/json:
          schema:
            oneOf:
              - $ref: "#/components/schemas/Error"
              - $ref: "#/components/schemas/ValidationError"
          example:
            message: Validation failed
            fields:
              new_value:
                field: new_value
                message: "'new_value' failed on the 'required' tag"
    RateLimited:
      description: Too many requests. WAFv2 Bot Control enforces partner rate limits at API Gateway.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            message: rate limit exceeded
    ServerError:
      description: Unhandled server error. Retry with exponential backoff.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            message: internal server error
    OAuthError:
      description: |
        RFC 6749 §5.2 token-endpoint error. Returned by `/v1/oauth/token` for
        all 4xx outcomes (invalid grant, expired authorization code, missing
        PKCE verifier, mismatched redirect URI, etc.). Inspect `error` rather
        than relying on the message text.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/OAuthErrorResponse"
          example:
            error: invalid_grant
            error_description: authorization code has expired
    OAuthUpstreamError:
      description: |
        Upstream identity provider (Cognito) returned a non-2xx response or was
        unreachable. Always uses the RFC 6749 envelope. Retry with exponential
        backoff; sustained failures should escalate to Movmo support.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/OAuthErrorResponse"
          example:
            error: server_error

paths:
  /.well-known/oauth-authorization-server:
    get:
      tags: [Authentication]
      summary: RFC 8414 authorization-server metadata
      description: |
        Publicly reachable discovery endpoint. Partner OAuth client libraries
        use this to auto-configure the authorization and token endpoints.
        Returns the same body as `/.well-known/openid-configuration`.
      operationId: getOAuthAuthorizationServerMetadata
      security: []
      responses:
        "200":
          description: Authorization-server metadata.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OAuthDiscoveryMetadata"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/ServerError"

  /.well-known/openid-configuration:
    get:
      tags: [Authentication]
      summary: OIDC discovery metadata
      description: |
        OIDC discovery alias. Serves the same payload as
        `/.well-known/oauth-authorization-server`.
      operationId: getOpenIdConfiguration
      security: []
      responses:
        "200":
          description: Discovery metadata.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OAuthDiscoveryMetadata"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/ServerError"

  /v1/oauth/token:
    post:
      tags: [Authentication]
      summary: Exchange authorization code or refresh token for an access token
      description: |
        Token endpoint for the OAuth 2.0 PKCE flow. This is the only OAuth
        endpoint in the partner surface — `/v1/oauth/code` is called by
        `auth.movmo.io` during the interactive authorization step and
        `/v1/oauth/register` is reserved for autonomous MCP agents using
        Dynamic Client Registration (RFC 7591).

        Accepts both `application/json` and `application/x-www-form-urlencoded`
        bodies for RFC 6749 compliance. Authorization codes are single-use and
        expire 10 minutes after issuance.
      operationId: exchangeOAuthToken
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/OAuthTokenRequest"
            examples:
              authorizationCode:
                summary: Authorization code exchange
                value:
                  grant_type: authorization_code
                  code: Rkm8b8Z5wV4-bwGqXh0pf3Cx7yl4T8TlrzaUeY_vBLM
                  code_verifier: wWw9RT4zFOSK6rS6rnK5Bq5bQ5dXp4d5y5f5j5Q5dXp
                  client_id: aviare-prod
                  redirect_uri: https://partner.example.com/oauth/callback
              refreshToken:
                summary: Refresh token grant
                value:
                  grant_type: refresh_token
                  client_id: aviare-prod
                  refresh_token: eyJjdHkiOi...refresh
          application/x-www-form-urlencoded:
            schema:
              $ref: "#/components/schemas/OAuthTokenRequest"
      responses:
        "200":
          description: Tokens issued successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OAuthTokenResponse"
              example:
                access_token: eyJraWQi...accesstoken
                refresh_token: eyJjdHkiOi...refresh
                token_type: Bearer
                expires_in: 900
        "400":
          $ref: "#/components/responses/OAuthError"
        "500":
          $ref: "#/components/responses/OAuthUpstreamError"
        "502":
          $ref: "#/components/responses/OAuthUpstreamError"

  /v1/users/me:
    get:
      tags: [Profile]
      summary: Get the authenticated user's profile
      description: |
        Preferred entry point for reading the current user — no UUID is
        exposed in the URL. Returns the full user record keyed off the bearer
        token's `custom:user_id` claim.
      operationId: getCurrentUser
      responses:
        "200":
          description: Current user profile.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /v1/users/{userid}:
    parameters:
      - name: userid
        in: path
        required: true
        schema:
          type: string
          format: uuid
        description: User identifier. Must be the caller's own user ID unless the caller is an admin.
    get:
      tags: [Profile]
      summary: Get a user's profile
      description: |
        Retrieves a user by UUID. Non-admin callers may only read their own
        record; cross-user reads return 403.
      operationId: getUser
      responses:
        "200":
          description: User profile.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"
    put:
      tags: [Profile]
      summary: Update a user's profile
      description: |
        Updates profile fields (name, suffix, gender, DOB, KTN, profile photo).
        The `email` and `phone_number` fields are **stripped from the request**
        server-side — callers must use the contact-change flow under
        `/v1/users/me/contact/*` to change either. Re-fetch the profile via
        `GET /v1/users/me` if the updated record is needed.
      operationId: updateUser
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateUserRequest"
            example:
              first_name: Jane
              middle_name: Ada
              last_name: Traveler
              suffix: Jr
              date_of_birth: "1988-07-12"
              gender: female
              known_traveler_number: "123456789"
      responses:
        "204":
          description: Profile updated. Empty body — re-fetch via `GET /v1/users/me` if needed.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /v1/users/me/contact/initiate:
    post:
      tags: [Profile]
      summary: Start an email or phone change
      description: |
        Step 1 of the three-step contact-change flow. Validates the new value,
        checks uniqueness, and sends an identity-verification OTP to the user's
        current email address (email OTP is always used for this step, even
        when changing the phone).
      operationId: initiateContactChange
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/InitiateContactChangePayload"
            example:
              contact_type: email
              new_value: jane.traveler+new@example.com
      responses:
        "200":
          description: Identity-verification OTP dispatched. Use the returned session to call `/v1/users/me/contact/verify-identity`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InitiateContactChangeResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          $ref: "#/components/responses/Conflict"
        "500":
          $ref: "#/components/responses/ServerError"

  /v1/users/me/contact/verify-identity:
    post:
      tags: [Profile]
      summary: Verify the identity OTP and dispatch a code to the new contact
      description: |
        Step 2 of the contact-change flow. Verifies the identity OTP, issues
        fresh session cookies, and then dispatches a separate verification code
        to the new email or phone. For email changes this uses Cognito's native
        verification; for phone changes a code is generated, hashed into
        DynamoDB, and sent via AWS End User Messaging SMS.

        Returns the access token in the response body rather than as a cookie
        to keep the Cookie header under API Gateway's 10 KB request-header
        limit. Pass the returned `access_token` back in
        `ConfirmContactChangePayload`.
      operationId: verifyContactChangeIdentity
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/VerifyIdentityPayload"
            example:
              code: "423981"
              session: AYABeF...ZZ
              contact_type: email
              new_value: jane.traveler+new@example.com
      responses:
        "200":
          description: Identity verified. Code dispatched to new contact.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VerifyIdentityResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          $ref: "#/components/responses/Conflict"
        "500":
          $ref: "#/components/responses/ServerError"

  /v1/users/me/contact/confirm:
    post:
      tags: [Profile]
      summary: Confirm the code sent to the new contact
      description: |
        Step 3 of the contact-change flow. Verifies the code sent to the new
        email or phone, writes the change through Cognito, and syncs the new
        value to Postgres.
      operationId: confirmContactChange
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ConfirmContactChangePayload"
            example:
              code: "817294"
              contact_type: email
              access_token: eyJraWQi...lvAg
              new_value: jane.traveler+new@example.com
      responses:
        "200":
          description: |
            Contact change completed. Cookies are refreshed so the new identity
            is reflected on subsequent requests; the response body returns the
            updated user record.
          content:
            application/json:
              schema:
                type: object
                properties:
                  user:
                    $ref: "#/components/schemas/User"
                required:
                  - user
              example:
                user:
                  id: 00000000-0000-0000-0000-000000000001
                  first_name: Jane
                  last_name: Traveler
                  email: jane.traveler+new@example.com
                  phone_number: "+15550142"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/ServerError"

  /v1/users/{userid}/locale-preferences:
    parameters:
      - name: userid
        in: path
        required: true
        schema:
          type: string
          format: uuid
    get:
      tags: [Profile]
      summary: Get a user's locale preferences
      description: |
        Returns the user's language, region, and currency. Always returns
        `200` with defaults (`en`/`US`/`USD`) when the user has never set
        preferences — preferences themselves never `404`. The `404` response
        only fires when the user in the URL does not exist.
      operationId: getLocalePreferences
      responses:
        "200":
          description: Locale preferences.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LocalePreferences"
              example:
                user_id: 00000000-0000-0000-0000-000000000001
                language_code: en
                region_code: US
                currency_code: USD
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: The user in the URL does not exist. Preferences themselves never 404 — defaults are returned instead.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                message: user not found
        "500":
          $ref: "#/components/responses/ServerError"
    put:
      tags: [Profile]
      summary: Upsert a user's locale preferences
      description: Upsert all three preference fields. ISO 639-1 language, ISO 3166-1 alpha-2 region, ISO 4217 currency.
      operationId: upsertLocalePreferences
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpsertLocalePreferencesRequest"
            example:
              language_code: es
              region_code: AR
              currency_code: USD
      responses:
        "200":
          description: Locale preferences saved.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LocalePreferences"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: The user in the URL does not exist.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                message: user not found
        "500":
          $ref: "#/components/responses/ServerError"

  /v1/users/{userid}/travel-preferences:
    parameters:
      - name: userid
        in: path
        required: true
        schema:
          type: string
          format: uuid
    get:
      tags: [Profile]
      summary: Get a user's travel preferences
      description: |
        Returns seat, bag, meal, and special-assistance preferences. Always
        returns `200` with empty defaults when the user has never set
        preferences — preferences themselves never `404`. The `404` response
        only fires when the user in the URL does not exist.
      operationId: getTravelPreferences
      responses:
        "200":
          description: Travel preferences.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TravelPreferences"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: The user in the URL does not exist. Preferences themselves never 404 — empty defaults are returned instead.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                message: user not found
        "500":
          $ref: "#/components/responses/ServerError"
    put:
      tags: [Profile]
      summary: Upsert a user's travel preferences
      description: |
        Upserts the complete preferences record. Nested structs (seat, bag,
        meal) must be fully specified — the handler does not merge partial
        payloads against the existing record.
      operationId: upsertTravelPreferences
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpsertTravelPreferencesRequest"
            example:
              seat:
                seat_type: window
                seating_zone: front
                extra_legroom: true
                exit_row: false
              bag:
                check_bag: true
              meal:
                meal_types: [vegetarian]
                dietary_notes: No shellfish
              special_assistance: []
      responses:
        "200":
          description: Travel preferences saved.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TravelPreferences"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: The user in the URL does not exist.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                message: user not found
        "500":
          $ref: "#/components/responses/ServerError"

  /v1/users/{userid}/passengers:
    parameters:
      - name: userid
        in: path
        required: true
        schema:
          type: string
          format: uuid
    get:
      tags: [Passengers]
      summary: List a user's saved passengers
      description: |
        Returns all active (non-soft-deleted) passengers for the user.
        When the optional `departure_date` query parameter is supplied, the
        server recalculates `traveler_type` for each passenger relative to
        that date. This is useful when listing passengers during a booking
        flow — a 12-year-old today may be an adult on a flight two years out.
      operationId: listPassengers
      parameters:
        - name: departure_date
          in: query
          required: false
          schema:
            type: string
            format: date
          description: Optional date to use for `traveler_type` calculation (YYYY-MM-DD).
          example: "2027-09-01"
      responses:
        "200":
          description: List of saved passengers.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Passenger"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"
    post:
      tags: [Passengers]
      summary: Create a passenger
      description: Creates a saved passenger owned by the user. Returns the new passenger ID.
      operationId: createPassenger
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/Passenger"
            example:
              first_name: Marco
              middle_name: Luiz
              last_name: Aviator
              gender: male
              date_of_birth: "1990-05-20"
              email: marco.aviator@example.com
              phone_number: "+15550198"
              nationality: US
              is_default: false
      responses:
        "201":
          description: Passenger created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreatedIdResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "500":
          $ref: "#/components/responses/ServerError"

  /v1/users/{userid}/passengers/{passengerid}:
    parameters:
      - name: userid
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: passengerid
        in: path
        required: true
        schema:
          type: string
          format: uuid
    get:
      tags: [Passengers]
      summary: Get a single passenger
      operationId: getPassenger
      responses:
        "200":
          description: Passenger record.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Passenger"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"
    put:
      tags: [Passengers]
      summary: Replace a passenger
      description: |
        Full-body replace. Partial-field updates are not supported — clients
        must `GET` the current record, modify it, and `PUT` the complete
        document. Returns `204 No Content` on success.
      operationId: updatePassenger
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/Passenger"
      responses:
        "204":
          description: Passenger updated.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"
    delete:
      tags: [Passengers]
      summary: Soft-delete a passenger
      description: |
        Soft-delete only — the row is retained with `deleted_at` set and
        filtered out of subsequent list queries. Foreign keys from booking
        passenger rows use `RESTRICT` to preserve booking history, so this
        operation succeeds even for passengers tied to past bookings.
      operationId: deletePassenger
      responses:
        "204":
          description: Passenger soft-deleted.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /v1/users/{userid}/passengers/{passengerid}/identity-documents:
    parameters:
      - name: userid
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: passengerid
        in: path
        required: true
        schema:
          type: string
          format: uuid
    get:
      tags: [Passengers]
      summary: List a passenger's identity documents
      operationId: listIdentityDocuments
      responses:
        "200":
          description: List of identity documents.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/IdentityDocument"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"
    post:
      tags: [Passengers]
      summary: Create an identity document
      description: Attaches an identity document (passport, visa, KTN, redress number, etc.) to a passenger. Document numbers are encrypted at rest with KMS.
      operationId: createIdentityDocument
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/IdentityDocument"
            example:
              document_type: passport
              document_number: A12345678
              issuing_country_code: US
              issuing_authority: U.S. Department of State
              issued_date: "2020-03-15"
              expires_on: "2030-03-14"
              nationality_on_document: US
              birth_country: US
              is_primary: true
      responses:
        "201":
          description: Identity document created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreatedIdResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "500":
          $ref: "#/components/responses/ServerError"

  /v1/users/{userid}/passengers/{passengerid}/identity-documents/{documentid}:
    parameters:
      - name: userid
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: passengerid
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: documentid
        in: path
        required: true
        schema:
          type: string
          format: uuid
    get:
      tags: [Passengers]
      summary: Get an identity document
      operationId: getIdentityDocument
      responses:
        "200":
          description: Identity document.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IdentityDocument"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"
    put:
      tags: [Passengers]
      summary: Update an identity document
      description: Full-body replace. Returns `204 No Content` on success.
      operationId: updateIdentityDocument
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/IdentityDocument"
      responses:
        "204":
          description: Identity document updated.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "500":
          $ref: "#/components/responses/ServerError"
    delete:
      tags: [Passengers]
      summary: Delete an identity document
      operationId: deleteIdentityDocument
      responses:
        "204":
          description: Identity document deleted.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /v1/users/{userid}/payment-methods:
    parameters:
      - name: userid
        in: path
        required: true
        schema:
          type: string
          format: uuid
    get:
      tags: [Payments]
      summary: List a user's payment methods
      operationId: listPaymentMethods
      responses:
        "200":
          description: List of payment methods.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/PaymentMethod"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"
    post:
      tags: [Payments]
      summary: Create a payment method
      description: |
        Attaches a Stripe payment method to the user's Stripe customer, then
        persists the brand/last4/exp metadata locally. The card PAN never
        touches Movmo — tokenize client-side with Stripe Elements and submit
        the resulting `paymentMethodId` + `customerId` here.
      operationId: createPaymentMethod
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PaymentMethod"
            example:
              customerId: cus_TestAviarePartner0001
              paymentMethodId: pm_TestVisa41111111111234
              methodType: credit_card
              token: tok_Test411111
              last4: "1234"
              brand: visa
              expMonth: 12
              expYear: 2030
              isDefault: true
      responses:
        "201":
          description: Payment method created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreatedIdResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "500":
          $ref: "#/components/responses/ServerError"

  /v1/users/{userid}/payment-methods/{methodid}:
    parameters:
      - name: userid
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: methodid
        in: path
        required: true
        schema:
          type: string
          format: uuid
    get:
      tags: [Payments]
      summary: Get a payment method
      operationId: getPaymentMethod
      responses:
        "200":
          description: Payment method.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaymentMethod"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"
    put:
      tags: [Payments]
      summary: Update a payment method (expiration / default only)
      description: |
        Accepts only `expMonth`, `expYear`, and `isDefault` — rekeying a card
        requires creating a new payment method. Propagates to Stripe before
        updating the local record.
      operationId: updatePaymentMethod
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PaymentMethodEdit"
            example:
              expMonth: 11
              expYear: 2031
              isDefault: false
      responses:
        "204":
          description: Payment method updated.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"
    delete:
      tags: [Payments]
      summary: Delete a payment method
      description: Detaches the method from the Stripe customer before removing the local row.
      operationId: deletePaymentMethod
      responses:
        "204":
          description: Payment method deleted.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /v1/flights/providers:
    get:
      tags: [Flights]
      summary: List all configured flight providers
      operationId: listProviders
      responses:
        "200":
          description: List of providers.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Provider"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/ServerError"

  /v1/flights/providers/active:
    get:
      tags: [Flights]
      summary: List active flight providers
      description: "Providers with `is_active: true`, sorted by search priority."
      operationId: listActiveProviders
      responses:
        "200":
          description: List of active providers.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Provider"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/ServerError"

  /v1/flights/providers/{provider}:
    parameters:
      - name: provider
        in: path
        required: true
        schema:
          type: string
          enum: [amadeus, duffel, dohop, darwin]
        description: Provider identifier. Darwin is the primary path for current Aviare integrations.
    get:
      tags: [Flights]
      summary: Get a specific provider
      operationId: getProvider
      responses:
        "200":
          description: Provider metadata.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Provider"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /v1/flights/providers/{provider}/capabilities:
    parameters:
      - name: provider
        in: path
        required: true
        schema:
          type: string
          enum: [amadeus, duffel, dohop, darwin]
    get:
      tags: [Flights]
      summary: Get a provider's capabilities (API-key-only)
      description: |
        **API-key-only auth. No user JWT required.** Capabilities describe
        provider-level features (direct booking, seat selection, hold booking,
        passenger and connection limits) and are loaded pre-auth by both
        `flights-ui` and partner integrations to decide which booking steps
        apply.
      operationId: getProviderCapabilities
      security:
        - ApiKeyAuth: []
      responses:
        "200":
          description: Provider capabilities.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProviderCapabilities"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /v1/flights/providers/{provider}/booking-flows:
    parameters:
      - name: provider
        in: path
        required: true
        schema:
          type: string
          enum: [amadeus, duffel, dohop, darwin]
    get:
      tags: [Flights]
      summary: Get a provider's supported booking flows
      description: Returns an array of supported booking-flow identifiers (e.g., `direct`, `hold_then_confirm`, `pricing_then_book`).
      operationId: getProviderBookingFlows
      responses:
        "200":
          description: List of supported booking flows.
          content:
            application/json:
              schema:
                type: array
                items:
                  type: string
                example: [direct, hold_then_confirm]
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /v1/flights/providers/{provider}/offers:
    parameters:
      - name: provider
        in: path
        required: true
        schema:
          type: string
          enum: [amadeus, duffel, dohop, darwin]
        description: Provider identifier. Darwin is the primary path for current Aviare integrations.
    post:
      tags: [Flights]
      summary: Create an offer from a provider-native payload (API-key-only)
      description: |
        **API-key-only auth. No user JWT required.** Entry point for the
        partner booking flow — partners pass a provider-native offer struct
        here after running their own provider search. The body shape is
        provider-specific: for Darwin this is the availability-response object
        from `/perform-availability`; for Amadeus it is a flight-offer object.

        Returns the Movmo-assigned `offer_id` that partners reference on
        subsequent calls to price, list passengers, and book.
      operationId: createProviderOffer
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProviderOfferInput"
      responses:
        "201":
          description: Offer created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreateOfferResponse"
              example:
                offer_id: off_darwin_5iA9kC
        "400":
          $ref: "#/components/responses/BadRequest"
        "500":
          $ref: "#/components/responses/ServerError"

  /v1/flights/providers/{provider}/offers/{offer_id}:
    parameters:
      - name: provider
        in: path
        required: true
        schema:
          type: string
          enum: [amadeus, duffel, dohop, darwin]
      - name: offer_id
        in: path
        required: true
        schema:
          type: string
        description: Movmo-assigned offer identifier.
    get:
      tags: [Flights]
      summary: Get an offer (API-key-only)
      description: |
        **API-key-only auth. No user JWT required.** Returns a normalized
        Movmo offer object. Used by both the MCP `get_offer_details` tool
        and partner booking flows.
      operationId: getProviderOffer
      security:
        - ApiKeyAuth: []
      responses:
        "200":
          description: Normalized offer.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Offer"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"
    put:
      tags: [Flights]
      summary: Update an offer with a refreshed provider payload
      description: |
        Replaces the stored provider-native offer data. Used for providers
        that persist offers locally (Amadeus) when the partner needs to push
        an updated payload. Returns `204 No Content`.
      operationId: updateProviderOffer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProviderOfferInput"
      responses:
        "204":
          description: Offer updated.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /v1/flights/providers/{provider}/offers/{offer_id}/pricing:
    parameters:
      - name: provider
        in: path
        required: true
        schema:
          type: string
          enum: [amadeus, duffel, dohop, darwin]
      - name: offer_id
        in: path
        required: true
        schema:
          type: string
    post:
      tags: [Flights]
      summary: Price an offer
      description: |
        Runs the provider's pricing step for providers that require one
        (Amadeus, Dohop). No-op for direct-booking providers (Duffel, Darwin).
        Returns the offer with refreshed pricing and condition details.
      operationId: priceProviderOffer
      responses:
        "200":
          description: Priced offer.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Offer"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /v1/flights/providers/{provider}/offers/{offer_id}/passengers:
    parameters:
      - name: provider
        in: path
        required: true
        schema:
          type: string
          enum: [amadeus, duffel, dohop, darwin]
      - name: offer_id
        in: path
        required: true
        schema:
          type: string
    get:
      tags: [Flights]
      summary: List passengers attached to an offer
      operationId: listOfferPassengers
      responses:
        "200":
          description: List of offer passengers.
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  additionalProperties: true
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"
    post:
      tags: [Flights]
      summary: Associate saved passengers with an offer
      description: |
        Links saved user passengers to the provider-assigned passenger IDs on
        the offer. Partners pass one mapping per offer passenger. Returns
        `200 OK` on success.
      operationId: saveOfferPassengers
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: array
              items:
                $ref: "#/components/schemas/OfferPassengerMapping"
            example:
              - user_passenger_id: 11111111-1111-1111-1111-111111111111
                provider_passenger_id: pas_dar_00009hj8USM7Ncg31cBCL
      responses:
        "200":
          description: Passengers associated.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /v1/flights/providers/{provider}/offers/{offer_id}/passengers/{passenger_id}:
    parameters:
      - name: provider
        in: path
        required: true
        schema:
          type: string
          enum: [amadeus, duffel, dohop, darwin]
      - name: offer_id
        in: path
        required: true
        schema:
          type: string
      - name: passenger_id
        in: path
        required: true
        schema:
          type: string
    put:
      tags: [Flights]
      summary: Update a single offer passenger
      description: |
        Updates fields on an offer passenger — typically adding KTN, loyalty
        accounts, or identity documents. Returns the offer with refreshed
        passenger data.
      operationId: updateOfferPassenger
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PassengerUpdateRequest"
      responses:
        "200":
          description: Updated offer.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Offer"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /v1/flights/providers/{provider}/offers/{offer_id}/seat-maps:
    parameters:
      - name: provider
        in: path
        required: true
        schema:
          type: string
          enum: [amadeus, duffel, dohop, darwin]
      - name: offer_id
        in: path
        required: true
        schema:
          type: string
    get:
      tags: [Flights]
      summary: Get seat maps for an offer
      description: |
        Returns an array of seat maps — one per segment that supports seat
        selection. Providers that do not support seat selection return an
        empty array.
      operationId: getOfferSeatMaps
      responses:
        "200":
          description: Seat maps.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/SeatMap"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /v1/flights/providers/{provider}/bookings:
    parameters:
      - name: provider
        in: path
        required: true
        schema:
          type: string
          enum: [amadeus, duffel, dohop, darwin]
        description: Darwin is the primary path for current Aviare integrations.
    post:
      tags: [Flights]
      summary: Create a booking
      description: |
        Creates a booking against the named provider. `idempotency_key` is
        required in the request body — repeat calls with the same key return
        the existing confirmed booking rather than double-booking. Creates
        and confirms a Stripe payment intent server-side before dispatching
        the booking request to the provider. Partners should expect the WAF
        to rate-limit bursts and retry with exponential backoff on `429`.
      operationId: createProviderBooking
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateBookingRequest"
            example:
              offer_id: off_darwin_5iA9kC
              idempotency_key: 55555555-5555-5555-5555-555555555555
              user_id: 00000000-0000-0000-0000-000000000001
              currency: USD
              payment_method_id: 33333333-3333-3333-3333-333333333333
              customer_id: cus_TestAviarePartner0001
              contact_info:
                email: jane.traveler@example.com
                phone_number: "+15550142"
                first_name: Jane
                last_name: Traveler
              passengers:
                - first_name: Jane
                  last_name: Traveler
                  gender: female
                  born_on: "1988-07-12"
                  traveler_type: adult
      responses:
        "200":
          description: Booking created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BookingResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/ServerError"

  /v1/flights/providers/{provider}/bookings/retry:
    parameters:
      - name: provider
        in: path
        required: true
        schema:
          type: string
          enum: [amadeus, duffel, dohop, darwin]
    post:
      tags: [Flights]
      summary: Retry a booking after a price change
      description: |
        Used when the original `POST /bookings` failed with a price-change
        error. The `order_id` from the `PriceChangeError` must be passed in
        the body alongside the original booking request so the provider can
        resume the two-phase flow with the new price.
      operationId: retryProviderBooking
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RetryBookingRequest"
      responses:
        "200":
          description: Booking retried.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BookingResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/ServerError"

  /v1/flights/providers/{provider}/bookings/{booking_id}:
    parameters:
      - name: provider
        in: path
        required: true
        schema:
          type: string
          enum: [amadeus, duffel, dohop, darwin]
      - name: booking_id
        in: path
        required: true
        schema:
          type: string
    get:
      tags: [Flights]
      summary: Get a booking by provider ID
      description: |
        Returns the provider-refreshed booking. Ownership is verified against
        the authenticated user — cross-user reads return 403.
      operationId: getProviderBooking
      responses:
        "200":
          description: Booking.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Booking"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"
    delete:
      tags: [Flights]
      summary: Cancel a booking
      description: |
        Initiates cancellation through the provider. Returns cancellation
        details including any refund amount.

        **Authorization caveat (MOVMO-347, tracked separately):** the handler
        currently authorizes on the `bookings:delete` permission only and does
        **not** verify that the booking belongs to the calling user. Cross-user
        cancellation is therefore possible for any caller with the permission
        until the ownership check ships. Partner clients should still treat
        this as a self-service endpoint and pass only their own booking IDs.
      operationId: cancelProviderBooking
      responses:
        "200":
          description: Booking cancelled.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CancellationResult"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /v1/users/{userid}/bookings:
    parameters:
      - name: userid
        in: path
        required: true
        schema:
          type: string
          format: uuid
    get:
      tags: [Flights]
      summary: List a user's bookings
      description: |
        Returns lightweight `TripSummary` entries for the user's bookings
        across all providers — denormalized for fast list rendering with a
        computed `status` (`upcoming` / `completed` / `cancelled`). Use
        `GET /v1/users/{userid}/bookings/{bookingid}` to read the full
        `Booking` record (payment, offer, idempotency fields).

        This is the blessed path for listing a user's trips — the
        `/v1/flights/users/{userid}/bookings` duplicate is out of scope and
        scheduled for deprecation.
      operationId: listUserBookings
      responses:
        "200":
          description: List of trip summaries.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/TripSummary"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /v1/users/{userid}/bookings/{bookingid}:
    parameters:
      - name: userid
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: bookingid
        in: path
        required: true
        schema:
          type: string
          format: uuid
    get:
      tags: [Flights]
      summary: Get a user's booking
      operationId: getUserBooking
      responses:
        "200":
          description: Booking.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Booking"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /v1/users/{userid}/bookings/{bookingid}/passengers:
    parameters:
      - name: userid
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: bookingid
        in: path
        required: true
        schema:
          type: string
          format: uuid
    get:
      tags: [Flights]
      summary: Get booking passengers
      description: Returns the passengers attached to a booking, enriched with ticket number, seat number, fare basis, and any seat selections.
      operationId: getUserBookingPassengers
      responses:
        "200":
          description: List of booking passengers.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/BookingPassenger"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /v1/users/{userid}/bookings/{bookingid}/offer:
    parameters:
      - name: userid
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: bookingid
        in: path
        required: true
        schema:
          type: string
          format: uuid
    get:
      tags: [Flights]
      summary: Get the offer behind a booking
      description: |
        Returns the offer associated with the booking. Falls back to the
        provider's native offer ID when Movmo's internal `flight_offer_id` is
        unavailable (e.g., providers that don't persist offers locally). If
        both lookups fail the offer is considered expired and `404` is
        returned.
      operationId: getUserBookingOffer
      responses:
        "200":
          description: Offer.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Offer"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"
