openapi: 3.1.0
info:
  title: CDS Groupe Hotel Booking API
  version: "1.0.0"
  summary: REST API for hotel search, availability, content, booking, cancellation and synchronization.
  description: |
    # Welcome to the CDS Developers Portal

    Welcome to the **CDS Groupe Hotel Booking API** documentation. Here you'll find
    everything needed to integrate hotel search, availability, booking and
    post-booking flows.

    | Environment | Base URL |
    |-------------|----------|
    | **Live**    | `https://bookings.cdsgroupe.com/cds-api/v1` |
    | **Sandbox** | `https://bookings.cdsgroupe.com/cds-api-test/v1` |

    ## Available API Services

    | Service     | Purpose                                              |
    |-------------|------------------------------------------------------|
    | **AUTH**    | Obtain a JWT token (mandatory before any call)       |
    | **SEARCH**  | Search hotels by location and availability           |
    | **AVAIL**   | Get rooms / rates available for a specific hotel     |
    | **CONTENT** | Retrieve detailed property information               |
    | **BOOK**    | Create a booking                                     |
    | **DETAILS** | Read existing bookings                               |
    | **CANCEL**  | Cancel an existing booking                           |
    | **SYNC**    | Synchronize a booking with the supplier              |

    ## Authentication

    All endpoints (except `POST /Authenticate`) require a JWT token in the
    `Authorization` header:

    ```
    Authorization: Bearer <token>
    ```

    Tokens are issued by `POST /Authenticate`. The default validity window
    is **~4 hours** (the actual TTL is returned in `TokenExpiration`).

    Request demo credentials from your CDS Groupe technical contact —
    no credentials are shipped in this specification.

    ## Error Response Format

    Errors share a common envelope:

    ```json
    {
      "ErrorCode": 132,
      "Message": "Room not available"
    }
    ```

    Most responses also include:

    | Field               | Type      | Meaning                                                   |
    |---------------------|-----------|-----------------------------------------------------------|
    | `Result`            | integer   | `1` = Success, `2` = Error, `3` = Warning, `4` = ScreenWarning |
    | `ApiHttpStatusCode` | integer   | Upstream HTTP status code (mirrors the response status)   |
    | `RequestedUri`      | string    | The exact URL processed by the API (useful for support)   |
    | `RequestTimeStamp`  | string    | ISO-8601 timestamp of the request                         |
    | `Message`           | string    | Human-readable status message                             |

    ## List of Error Codes

    | Code   | Description                                          |
    |--------|------------------------------------------------------|
    | 0      | No error                                             |
    | 2      | No service on requested provider                     |
    | 15     | Invalid date                                         |
    | 20     | Number of passengers invalid                         |
    | 61     | Invalid currency code                                |
    | 95     | Booking already cancelled                            |
    | 107    | Cannot book — too far in advance                     |
    | 111    | Booking invalid                                      |
    | 117    | Cannot access booking details                        |
    | 132    | Room not available                                   |
    | 137    | Adult numbers / occupancy mismatch                   |
    | 143    | Price incorrect for room / unit                      |
    | 190    | Wrong network — access denied                        |
    | 211    | Cannot make a reservation for this duration          |
    | 240    | Credit card has expired                              |
    | 241    | Expiration date is invalid                           |
    | 242    | Credit card number is invalid or missing             |
    | 297    | Unable to process — insufficient data                |
    | 320    | Invalid value                                        |
    | 352    | Invalid credit card type                             |
    | 366    | Error during processing, please retry                |
    | 427    | No rooms available for requested dates               |
    | 437    | Rate unavailable                                     |
    | 438    | Requested rate not available                         |
    | 446    | Service request not allowed                          |
    | 448    | System error                                         |
    | 450    | Unable to process                                    |
    | 506    | Credit card not accepted at property                 |
    | 580    | Problem with supplier                                |
    | 588    | Supplier price changed — payment amount mismatch     |
    | 589    | Booking not possible — supplier price mismatch       |
    | 599    | Booking not possible on Booking.com                  |
    | 1175   | Username or password invalid                         |
    | 1766   | Login not allowed                                    |
    | 1899   | Problem processing payment                           |
    | 1900   | Client disabled — access denied                      |
    | 2400   | Credit card holder invalid                           |
    | 3522   | Invalid credit card                                  |
    | 4477   | Unable to update                                     |
    | 44777  | Unable to insert                                     |
    | 5222   | Insufficient funds on credit card                    |

    ## Test Bookings

    To exercise the booking flow without committing a real reservation, send
    `"IsTestBooking": true` in the `POST /HotelRes` body. The response will
    contain an `RsvCd` prefixed with `MOCK-`.
  contact:
    name: CDS Groupe Technical Support
    url: https://www.cdsgroupe.com
  license:
    name: Proprietary
    identifier: LicenseRef-Proprietary

servers:
  - url: https://bookings.cdsgroupe.com/cds-api/v1
    description: Live (production)
  - url: https://bookings.cdsgroupe.com/cds-api-test/v1
    description: Sandbox (test)

tags:
  - name: AUTH
    description: Obtain a JWT token. Required before any other call.
  - name: SEARCH
    description: Search hotels by geographic location and availability.
  - name: AVAIL
    description: Get rooms and rates for a specific hotel.
  - name: CONTENT
    description: Retrieve property descriptions, images, amenities, etc.
  - name: BOOK
    description: Create a booking.
  - name: DETAILS
    description: Read existing bookings.
  - name: CANCEL
    description: Cancel an existing booking.
  - name: SYNC
    description: Synchronize a booking with the supplier system.

security:
  - bearerAuth: []

paths:
  /Authenticate:
    post:
      tags: [AUTH]
      summary: Obtain a JWT token
      operationId: authenticate
      security: []  # public endpoint
      description: |
        Exchanges credentials for a JWT token. Default validity is ~4 hours
        (exact TTL returned in `TokenExpiration`). The token must be sent
        as `Authorization: Bearer <token>` on every other call.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoginRequest'
            examples:
              default:
                summary: Standard login
                value:
                  Username: USER@CDSGROUPE.COM
                  Password: your-password
                  Language: EN
      responses:
        '200':
          description: Authentication successful
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiLoginResponse'
              examples:
                success:
                  summary: Token issued
                  externalValue: ./examples/response-authenticate.json
        '400':
          description: Invalid credentials — typically returns error code `1175`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'

  /hotelSearch:
    get:
      tags: [SEARCH]
      summary: Search hotels by location
      operationId: searchHotels
      description: |
        Returns a paginated list of hotels matching geographic criteria and
        availability for the given stay. Use the resulting `HtlCd` to drill
        into `GET /hotelAvail` or `GET /Hotel`.

        When the response carries `MoreResponses: true`, the wall-clock
        deadline cut the supplier fan-out before all suppliers replied —
        the partial list is still usable, and you can call `GET /hotelAvail`
        on the hotels you care about to obtain the complete rate list.
      parameters:
        - $ref: '#/components/parameters/AgentDutyCode'
        - $ref: '#/components/parameters/Language'
        - $ref: '#/components/parameters/Currency'
        - name: latitude
          in: query
          required: true
          schema: { type: number, format: double, example: 48.856614 }
          description: Search center latitude (WGS84).
        - name: longitude
          in: query
          required: true
          schema: { type: number, format: double, example: 2.3522219 }
          description: Search center longitude (WGS84).
        - name: radius
          in: query
          required: true
          schema: { type: integer, minimum: 1, maximum: 200, default: 10, example: 2 }
          description: Search radius. Max **200**. Unit depends on `radiusMeasure` (default `K` = kilometers, `M` = miles).
        - $ref: '#/components/parameters/Checkin'
        - $ref: '#/components/parameters/Checkout'
        - $ref: '#/components/parameters/Room'
        - $ref: '#/components/parameters/Guest'
        - $ref: '#/components/parameters/Allotment'
        - name: age
          in: query
          schema: { type: string, example: "99" }
          description: Guest age(s) — `99` for adult, `1`–`17` for child. Multiple ages may be passed separated by `;` or `,`.
        - name: ages
          in: query
          schema: { type: string, example: "99" }
          description: Alias for `age`. Same semantics.
        - name: hotelName
          in: query
          schema: { type: string, maxLength: 200, example: "ibis" }
          description: Filter by hotel name (partial match).
        - name: pageNumber
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 1, example: 1 }
          description: Page number (1-based).
        - name: pageSize
          in: query
          schema: { type: integer, minimum: 1, maximum: 1000, default: 100, example: 50 }
          description: Page size.
      responses:
        '200':
          description: Paginated search result.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiHotelResult'
              examples:
                marignan:
                  summary: 1-night stay near Notre-Dame
                  externalValue: ./examples/response-search.json
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '408':
          $ref: '#/components/responses/RequestTimeout'

  /hotelAvail:
    get:
      tags: [AVAIL]
      summary: Get rates for a specific hotel
      operationId: getAvailability
      description: |
        Returns rooms and rates available for one or more properties.
        Recommended follow-up to `GET /hotelSearch` to get the complete rate
        list. Multiple hotel codes can be passed in `hotelCode` separated by
        `;` or `,`.
      parameters:
        - $ref: '#/components/parameters/AgentDutyCode'
        - $ref: '#/components/parameters/Language'
        - $ref: '#/components/parameters/Currency'
        - name: hotelCode
          in: query
          required: true
          schema: { type: string, example: "AMDNNPAR650" }
          description: One or more `HtlCd` from SEARCH (`;` or `,` separated).
        - name: giataCode
          in: query
          schema: { type: string, example: "235605" }
          description: Optional GIATA code (`;` or `,` separated). If supplied, must match the number of hotelCode entries.
        - $ref: '#/components/parameters/Checkin'
        - $ref: '#/components/parameters/Checkout'
        - $ref: '#/components/parameters/Room'
        - $ref: '#/components/parameters/Guest'
        - $ref: '#/components/parameters/Allotment'
        - name: age
          in: query
          schema: { type: string, example: "99" }
          description: Guest age(s) — `99` for adult, `1`–`17` for child. Multiple separated by `;` or `,`.
        - name: ages
          in: query
          schema: { type: string, example: "99" }
          description: Alias for `age`.
        - name: ratePlanCandidates
          in: query
          schema: { type: string, example: "EPS442901/EPS/200169162_384135922_37316" }
          description: Optional `RatCd` list (`,` separated) to filter rates.
      responses:
        '200':
          description: Available rooms and rates.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiHotelAvailResult'
              examples:
                marignan:
                  summary: 1-night stay rates
                  externalValue: ./examples/response-avail.json
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /Hotel:
    get:
      tags: [CONTENT]
      summary: Get a hotel by code
      operationId: getHotelByCode
      description: Returns full content (description, images, amenities) for a single hotel.
      parameters:
        - name: code
          in: query
          required: true
          schema: { type: string, example: "6203406" }
          description: Hotel identifier (`HtlCd` — numeric CDS id or supplier-prefixed code).
        - name: lang
          in: query
          schema: { type: string, default: "EN", example: "EN" }
          description: Language code (`FR`, `EN`, `DE`, `ES`, `PT`, `IT`).
      responses:
        '200':
          description: Hotel content.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HotelContentResponse'
              examples:
                marignan:
                  summary: Hôtel Marignan
                  externalValue: ./examples/response-hotel.json
        '204':
          description: No hotel matched the given code.
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /Hotels:
    get:
      tags: [CONTENT]
      summary: List hotels by country prefix or free-text search
      operationId: getHotels
      description: |
        If `prefix` is **exactly 2 characters**, it is treated as an
        ISO 3166-1 alpha-2 country code and returns hotels for that country
        (use `from`/`to` for pagination, max page size 500). Otherwise the
        endpoint behaves as a free-text hotel-name search.
      parameters:
        - name: prefix
          in: query
          required: true
          schema: { type: string, example: "FR" }
          description: Country code (`FR`, `DE`…) when length is 2, otherwise hotel name (or prefix).
        - name: from
          in: query
          schema: { type: integer, minimum: 0, default: 0 }
          description: Pagination offset (country-list mode only).
        - name: to
          in: query
          schema: { type: integer, minimum: 1, maximum: 500, default: 500 }
          description: Pagination end (country-list mode only). Max 500.
        - name: fields
          in: query
          schema: { type: string }
          description: Optional comma-separated list of fields to keep in the response.
      responses:
        '200':
          description: Matching hotels.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HotelContentResponse'
              examples:
                marignan:
                  summary: Match on "HOTEL MARIGNAN"
                  externalValue: ./examples/response-hotels.json
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /HotelRes:
    post:
      tags: [BOOK]
      summary: Create a booking
      operationId: createBooking
      description: |
        Books a room previously identified through SEARCH / AVAIL. A successful
        booking returns `RsvCd`, `RsvCd2`, `RsvCd3` with `RsvStatus = 1`
        (`Reserved`).

        Set `IsTestBooking: true` to run the full booking pipeline without
        committing a real reservation. The returned `RsvCd` will be prefixed
        with `MOCK-`.

        Many fields accept **list values** separated by `;` or `,` — the list
        length must match `RoomQuantities` for rate/room/price fields, and
        `GuestQuantitie` for traveller fields.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProcessBooking'
            examples:
              default:
                summary: Booking with Visa
                externalValue: ./examples/request-book.json
      responses:
        '200':
          description: Booking processed (check `Result` and `Bookings[].RsvStatus`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiBookingResult'
              examples:
                success:
                  summary: Booking accepted
                  externalValue: ./examples/response-book.json
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '405':
          description: Duplicate itinerary (same booking token sent twice).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'

  /CancelBooking:
    post:
      tags: [CANCEL]
      summary: Cancel an existing booking
      operationId: cancelBooking
      description: Cancels a previously created booking by `RsvCd`. Multiple `codeBooking` may be passed (`;` or `,` separated).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CdsCancelModel'
            examples:
              default:
                summary: Cancel with reason
                externalValue: ./examples/request-cancel.json
      responses:
        '200':
          description: Cancellation processed (check `Result`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiBookingCancelResult'
              examples:
                success:
                  summary: Booking cancelled
                  externalValue: ./examples/response-cancel.json
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /readBookings:
    post:
      tags: [DETAILS]
      summary: Read bookings
      operationId: readBookings
      description: |
        Returns one or many bookings matching the search criteria.

        **`typeDate` filter (only applied when `start`/`end` are passed):**

        | Value | Filter on    |
        |-------|--------------|
        | `1`   | Check-in     |
        | `2`   | Check-out    |
        | `3`   | Booking date |

        **`status` filter (subset of `ReservationStatus`):**

        | Value | Meaning |
        |-------|---------|
        | `1`   | Reserved |
        | `2`   | Requested |
        | `5`   | Cancelled |
        | `10`  | Error |
        | `20`  | Payment refused |
        | `21`  | Not debited at check-in |
        | `22`  | Call failures |
        | `55`  | Cancelled by the hotel |
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CdsReadModel'
            examples:
              default:
                summary: Bookings from 2024-12-01 by check-in
                externalValue: ./examples/request-read.json
      responses:
        '200':
          description: List of bookings matching the criteria.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiBookingResult'
              examples:
                list:
                  summary: One booking returned
                  externalValue: ./examples/response-read.json
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /Sync:
    post:
      tags: [SYNC]
      summary: Synchronize a booking with the supplier
      operationId: syncBooking
      description: |
        Pulls the latest supplier state for one or more bookings and reconciles
        the CDS-side record. Multiple `codeBooking` may be passed (`;` or `,`
        separated). When the user doesn't have access to the synced booking
        an HTTP 405 with `AgentDutyCodeUnauthorized` is returned.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CdsSyncModel'
            examples:
              default:
                summary: Sync by RsvCd
                externalValue: ./examples/request-sync.json
      responses:
        '200':
          description: Synchronization processed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiBookingSyncResult'
              examples:
                success:
                  summary: Sync complete
                  externalValue: ./examples/response-sync.json
        '204':
          description: No matching booking found.
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '405':
          description: Booking not accessible with current credentials.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: JWT issued by `POST /Authenticate`.

  parameters:
    AgentDutyCode:
      name: agentDutyCode
      in: query
      required: true
      schema: { type: string, maxLength: 30, example: "CDS" }
      description: Client code provided by the CDS Groupe technical team.
    Language:
      name: language
      in: query
      schema:
        type: string
        enum: [FR, EN, DE, ES, PT, IT]
        default: EN
        example: FR
      description: ISO 639-1 language code.
    Currency:
      name: currency
      in: query
      schema: { type: string, maxLength: 3, default: "EUR", example: "EUR" }
      description: ISO 4217 currency code.
    Checkin:
      name: checkin
      in: query
      required: true
      schema: { type: string, format: date, example: "2024-12-20" }
      description: Check-in date (`yyyy-MM-dd`).
    Checkout:
      name: checkout
      in: query
      required: true
      schema: { type: string, format: date, example: "2024-12-21" }
      description: Check-out date (`yyyy-MM-dd`).
    Room:
      name: room
      in: query
      required: true
      schema: { type: string, default: "1", example: "1" }
      description: Number of rooms requested. Stringified integer.
    Guest:
      name: guest
      in: query
      required: true
      schema: { type: string, default: "1", example: "1" }
      description: Total number of guests. Stringified integer.
    Allotment:
      name: allotment
      in: query
      schema: { type: string, example: "1" }
      description: Allotment / guests per room (`;` or `,` separated).

  responses:
    BadRequest:
      description: Validation failed (missing field, invalid date, conflicting filters…).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    Unauthorized:
      description: Missing, invalid or expired JWT token.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    RequestTimeout:
      description: The wall-clock deadline elapsed before processing completed.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'

  # ===========================================================================
  # ENUMS (mirrored from the C# source-of-truth in /CDSWebAPI.Models and /Core)
  # ===========================================================================

  schemas:
    ServiceResult:
      type: integer
      enum: [0, 1, 2, 3, 4]
      description: |
        Generic outcome status (mirror of `Cds.DataObjects.Common.ServiceResult`).

        | Value | Meaning |
        |-------|---------|
        | `0`   | Unknown |
        | `1`   | Success |
        | `2`   | Error |
        | `3`   | Warning |
        | `4`   | Screen Warning |

    ReservationStatus:
      type: integer
      enum: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 99]
      description: |
        Booking lifecycle status (mirror of `Cds.DataObjects.HotelReservation.ReservationStatus`).

        | Value | Meaning |
        |-------|---------|
        | `0`   | Quoted (pre-book) |
        | `1`   | Reserved |
        | `2`   | Requested |
        | `3`   | Request denied |
        | `4`   | No-show |
        | `5`   | Cancelled |
        | `6`   | In-house |
        | `7`   | Checked out |
        | `8`   | Waitlisted |
        | `9`   | Active |
        | `10`  | Error |
        | `99`  | Waiting for approval (display only) |

    HtlType:
      type: integer
      enum: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
      description: |
        Property type (mirror of `Cds.Model.Enums.HtlType`).

        | Value | Meaning |
        |-------|---------|
        | `0`   | NotKnown |
        | `1`   | Hotel |
        | `2`   | Appart |
        | `3`   | AptHotel |
        | `4`   | Gite |
        | `5`   | Motel |
        | `6`   | Resort |
        | `7`   | Lodge |
        | `8`   | Insolite |
        | `9`   | Closed |
        | `10`  | BedBreakfast |
        | `11`  | NotKnownAppartment |

    PaymentType:
      type: integer
      description: |
        Payment method (mirror of `Cds.DataObjects.Ota.PaymentType` — OTA-compliant).
        Common values: `0` NotKnown, `1` Cash, `2` DirectBill, `3` Voucher,
        `4` PrePay, `5` CreditCard, `6` DebitCard, `7` Check, `8` Deposit,
        `9` BusinessAccount, `10` CentralBill, `31` Guarantee. The full
        OTA enum has 33 members — see CDS internal documentation for the rest.

    # =========================================================================
    # AUTH
    # =========================================================================

    LoginRequest:
      type: object
      required: [Username, Password]
      properties:
        Username:
          type: string
          maxLength: 80
          description: Username provided by CDS Groupe team (typically an email).
          example: USER@CDSGROUPE.COM
        Password:
          type: string
          format: password
          maxLength: 50
        Language:
          type: string
          enum: [FR, EN, DE, ES, PT, IT]
          default: EN
          description: User's preferred language for error messages.

    ApiLoginResponse:
      allOf:
        - $ref: '#/components/schemas/ResponseEnvelope'
        - type: object
          properties:
            Token:
              type: string
              description: JWT bearer token (HS256).
            AgentDutyCode:
              type: [string, "null"]
              description: Client code resolved from the credentials.
            AgentDutyName:
              type: [string, "null"]
              description: Display name of the agent duty (only returned when set).
            Login:
              type: [string, "null"]
            IsSso:
              type: boolean
              description: '`true` when the session was opened through SSO.'
            TokenExpiration:
              type: [integer, "null"]
              description: Token validity in hours (only returned when > 0).
            PaymentMethod:
              allOf:
                - $ref: '#/components/schemas/PaymentType'
              description: Default payment method for the agent (only returned when known).
            PaymentMethodDescription:
              type: string
              description: Human-readable payment method label (only returned when non-empty).

    # =========================================================================
    # COMMON ENVELOPES
    # =========================================================================

    ResponseEnvelope:
      type: object
      description: Fields present on every API response (mirror of `ApiModelBase`).
      properties:
        Result:
          $ref: '#/components/schemas/ServiceResult'
        ApiHttpStatusCode:
          type: integer
          description: Upstream HTTP status code.
        RequestedUri:
          type: string
        RequestTimeStamp:
          type: string
          format: date-time
        ErrorCode:
          type: integer
          description: See the error-code table in the introduction. Only serialized when non-zero.
        Message:
          type: string
          description: Human-readable status message.

    ErrorEnvelope:
      type: object
      description: Subset of `ResponseEnvelope` returned on error paths.
      properties:
        ErrorCode:
          type: integer
          description: See the error-code table in the introduction.
          example: 132
        Message:
          type: string
          example: Room not available
        ApiHttpStatusCode:
          type: integer
        RequestedUri:
          type: string

    # =========================================================================
    # SHARED REQUEST BASE (CdsApiBase)
    # =========================================================================

    CdsApiBase:
      type: object
      description: Fields shared by every authenticated request DTO (`CdsApiBase`).
      required: [agentDutyCode]
      properties:
        agentDutyCode:
          type: string
          maxLength: 30
          description: (REQUIRED) the client code given by CDS Groupe team.
          example: CDS
        language:
          type: string
          enum: [FR, EN, DE, ES, PT, IT]
          default: EN
        currency:
          type: string
          maxLength: 3
          default: EUR

    # =========================================================================
    # HOTEL CONTENT MODELS
    # =========================================================================

    ApiHotels:
      type: object
      description: A property record (`ApiHotels`). Fields marked **optional** are only serialized when populated.
      additionalProperties: true
      properties:
        HtlId:           { type: integer, description: CDS internal property id.}
        HtlCd:           { type: string,  description: Public property code.}
        GiataCode:       { type: string,  description: GIATA code (optional).}
        HtlIata:         { type: string,  description: IATA code (optional).}
        HtlName:         { type: string }
        HtlType:         { $ref: '#/components/schemas/HtlType' }
        HtlAddress1:     { type: string }
        CtyId:           { type: integer, description: CDS city id (optional).}
        HtlCity:         { type: string }
        HtlStateCode:    { type: string,  description: Property state code (optional).}
        HtlStateName:    { type: string,  description: Property state name (optional).}
        HtlZipCode:      { type: string }
        CntCd:           { type: string,  description: ISO 3166-1 alpha-2 country code.}
        HtlDesc1:        { type: string,  description: English description.}
        HtlDesc2:        { type: string,  description: Localized description.}
        CurCd:           { type: string,  description: Property local currency.}
        HtlStars:        { type: [integer, "null"], minimum: 0, maximum: 5 }
        HchCd:           { type: string,  description: Chain code.}
        HbrId:           { type: [integer, "null"], description: 'Property brand id (optional, > 0).' }
        HbrParentId:     { type: [integer, "null"], description: Parent brand id (optional).}
        Distance:        { type: [number, "null"], format: double, description: Distance from search center.}
        HtlLongitude:    { type: [number, "null"], format: double }
        HtlLatitude:     { type: [number, "null"], format: double }
        HtlPhone:        { type: string }
        HtlEmail:        { type: string, format: email }
        HtlNameSearch:   { type: string, description: Normalized hotel name used for matching.}
        ImageUrl:        { type: string, format: uri, description: Primary image.}
        WebSite:         { type: string, format: uri }
        RoomCount:       { type: string }
        FloorsCount:     { type: string }
        BuildDate:       { type: string }
        RenovationDate:  { type: string }
        PetsAllowed:     { type: boolean }
        CheckInTime:     { type: string }
        CheckOutTime:    { type: string }
        HotelPaymentList: { type: array, items: { type: string } }
        DistancesFromPoints: { type: array, items: { $ref: '#/components/schemas/ApiGeoPoints' } }
        MealPrices:      { type: array, items: { $ref: '#/components/schemas/ApiMealPrices' } }
        RoomRateList:    { type: [array, "null"], items: { $ref: '#/components/schemas/ApiRoomRate' }, description: Property rates.}
        SanitaryInfoList: { type: array, items: { $ref: '#/components/schemas/ApiHotelInfo' } }
        HotelInfoList:   { type: array, items: { $ref: '#/components/schemas/ApiHotelInfo' } }
        RoomAmenityList: { type: array, items: { $ref: '#/components/schemas/ApiRoomAmenity' } }
        ReferencePoints: { type: array, items: { $ref: '#/components/schemas/ApiHotelInfo' } }
        AttractionPoints: { type: array, items: { $ref: '#/components/schemas/ApiHotelInfo' } }
        RecreationsPoints: { type: array, items: { $ref: '#/components/schemas/ApiHotelInfo' } }
        BusinessSrvcType: { type: array, items: { $ref: '#/components/schemas/ApiHotelInfo' } }
        GuestRoomInfo:   { type: array, items: { $ref: '#/components/schemas/ApiHotelInfo' } }
        ImageInfoList:   { type: array, items: { $ref: '#/components/schemas/ApiImageInfo' } }
        HotelInfoExtensions: { type: object, additionalProperties: { type: string } }

    ApiRoomRate:
      type: object
      description: A bookable rate for a specific room type. The full surface has 30+ fields; the most useful are listed here.
      additionalProperties: true
      properties:
        RatCd:                  { type: string, description: "Rate code — pass as `RatePlanCode` to BOOK." }
        RomCd:                  { type: string, description: "Room code — pass as `RoomCode` to BOOK." }
        MaxOccupancy:           { type: integer }
        RatQty:                 { type: integer, description: "Number of rooms left at this rate." }
        CurCd:                  { type: string }
        RatPrice:               { type: number, format: decimal }
        TaxPrice:               { type: number, format: decimal }
        RatMealPrice:           { type: number, format: decimal }
        RoomPrice:              { type: number, format: decimal }
        TotalPricePerRoom:      { type: number, format: decimal }
        TotalPricePerStay:      { type: number, format: decimal }
        RatName:                { type: string }
        RatDesc:                { type: string }
        RoomPriceNightly:       { type: array, items: { type: string }, description: "Per-night price strings." }
        RoomImagesList:         { type: array, items: { type: string, format: uri } }
        RateOrigin:             { type: integer, description: "Supplier id (see `HotelOrigin` table in CDS docs)." }
        RateInformations:       { type: array, items: { type: string } }
        IncludedFeatures:       { type: array, items: { type: string } }
        RatCancelPolicy:        { type: string }
        CancellationPolicyDate: { type: string, format: date-time }
        CancelPenaltyList:      { type: array, items: { type: string } }
        RatTax:                 { type: boolean }
        TaxIncluded:            { type: boolean }
        RatBreakfast:           { type: boolean }
        RatLunch:               { type: boolean }
        RatDinner:              { type: boolean }
        IsCancellable:          { type: boolean }
        IsDeposit:              { type: boolean }
        IsNegotiatedRate:       { type: boolean }
        IsCdsCorpoRate:         { type: boolean }
        IsAgodaRate:            { type: boolean }
        NegotiatedRate:         { type: boolean }
        IsPwd:                  { type: boolean }
        RoomRateCompliance:     { type: integer }
        RateTypePreference:     { type: integer }

    ApiHotelInfo:
      type: object
      description: OTA-standard taxonomy entry (amenities, sanitary info, points of interest…).
      properties:
        OtaType: { type: string, example: HAC }
        OtaCd:   { type: integer }
        OtaCode:
          type: object
          properties:
            OtaName:  { type: string, description: English label.}
            OtaName2: { type: string, description: French label.}

    ApiRoomAmenity:
      $ref: '#/components/schemas/ApiHotelInfo'

    ApiImageInfo:
      type: object
      properties:
        ImgFile: { type: string, format: uri }

    ApiGeoPoints:
      type: object
      additionalProperties: true
      properties:
        Name:     { type: string }
        Distance: { type: number, format: double }
        Latitude: { type: number, format: double }
        Longitude: { type: number, format: double }

    ApiMealPrices:
      type: object
      additionalProperties: true
      properties:
        Code:  { type: string }
        Price: { type: number, format: decimal }

    # =========================================================================
    # SEARCH / AVAIL / CONTENT responses
    # =========================================================================

    ApiHotelResult:
      allOf:
        - $ref: '#/components/schemas/ResponseEnvelope'
        - type: object
          properties:
            NumberOfResult: { type: integer, description: Hotels returned in this page.}
            MoreResponses:
              type: boolean
              description: '`true` when the wall-clock deadline cut the supplier fan-out before all suppliers replied.'
            CurrentPage:    { type: integer }
            PageSize:       { type: integer }
            TotalCount:     { type: integer }
            TotalPages:     { type: integer }
            PreviousPage:   { type: string, enum: ["Yes", "No"] }
            NextPage:       { type: string, enum: ["Yes", "No"] }
            MaxPrice:       { type: number, format: decimal }
            MinPrice:       { type: number, format: decimal }
            Currency:       { type: string }
            MaxDistance:    { type: number, format: decimal }
            MinDistance:    { type: number, format: decimal }
            Hotels:         { type: array, items: { $ref: '#/components/schemas/ApiHotels' } }

    ApiHotelAvailResult:
      allOf:
        - $ref: '#/components/schemas/ResponseEnvelope'
        - type: object
          properties:
            NumberOfResult: { type: integer }
            MoreResponses:  { type: boolean }
            MaxPrice:       { type: number, format: decimal }
            MinPrice:       { type: number, format: decimal }
            Currency:       { type: string }
            Hotels:         { type: array, items: { $ref: '#/components/schemas/ApiHotels' } }

    HotelContentResponse:
      allOf:
        - $ref: '#/components/schemas/ResponseEnvelope'
        - type: object
          properties:
            NumberOfHotels: { type: integer }
            ResumeKey:      { type: string, description: Opaque pagination cursor (only present when more results exist).}
            Hotels:         { type: array, items: { $ref: '#/components/schemas/ApiHotels' } }

    # =========================================================================
    # BOOK request (ProcessBooking)
    # =========================================================================

    ProcessBooking:
      allOf:
        - $ref: '#/components/schemas/CdsApiBase'
        - type: object
          required:
            - HotelCode
            - RatePlanCode
            - RoomCode
            - AmountAfterTax
            - AmountBeforeTax
            - Start
            - End
            - RoomQuantities
            - GuestQuantitie
            - GuestPerRoom
            - LastName
            - FirstName
            - Email
          properties:
            HotelCode:       { type: string,  description: '(REQUIRED) `HtlCd` from AVAIL.', example: AMDNNPAR650 }
            ChainCode:       { type: string,  description: '`HchCd` from AVAIL (send `NN` if null/empty).', default: NN, example: NN }
            GiataCode:       { type: string,  description: GIATA code from AVAIL., example: "235605" }
            RatePlanCode:    { type: string,  description: '(REQUIRED) `RatCd` from AVAIL — list separated by `;` or `,`, length must equal `RoomQuantities`.', example: "EPS442901/EPS/200169162_384135922_37316" }
            RoomCode:        { type: string,  description: '(REQUIRED) `RomCd` from AVAIL — list separated by `;` or `,`, length must equal `RoomQuantities`.', example: "EPS/200169162_384135922_37316" }
            AmountAfterTax:  { type: string,  description: '(REQUIRED) `RatPrice` + `TaxPrice` + `RatMealPrice` if `TaxIncluded=false`, else `RatPrice`. List `;` or `,` separated.', example: "80" }
            AmountBeforeTax: { type: string,  description: '(REQUIRED) `RatPrice` from AVAIL. List `;` or `,` separated, length must equal `RoomQuantities`.', example: "78" }
            CardCode:
              type: string
              description: Credit card code — `AX` (Amex), `MC` / `CA` (MasterCard), `VI` (Visa), `TP` (Travel Pro).
              enum: [AX, MC, CA, VI, TP]
              example: VI
            CardNumber:      { type: string, description: Credit card number., example: "4236861588423130" }
            ExpireDate:      { type: string, format: date, description: 'Credit card expiration (`yyyy-MM-dd`).', example: "2025-12-01" }
            SeriesCode:      { type: string, description: Credit card CVV / CVC., example: "123" }
            CardHolderName:  { type: string, example: TEST }
            Start:           { type: string, format: date, description: '(REQUIRED) Check-in (`yyyy-MM-dd`).', example: "2024-12-20" }
            End:             { type: string, format: date, description: '(REQUIRED) Check-out (`yyyy-MM-dd`).', example: "2024-12-21" }
            RoomQuantities:  { type: integer, minimum: 1, default: 1, description: '(REQUIRED) Number of rooms.', example: 1 }
            GuestQuantitie:  { type: integer, minimum: 1, default: 1, description: '(REQUIRED) Total guests.', example: 1 }
            GuestPerRoom:    { type: string,  description: '(REQUIRED) Guests per room (`;` or `,` separated).', example: "1" }
            ages:
              type: string
              description: Guest age(s) — `99` adult, `1`–`17` child (`;` or `,` separated).
              example: "99"
            allotment:       { type: string, description: 'Guests per room (`;` or `,` separated).', example: "1" }
            LastName:        { type: string, description: '(REQUIRED) Last names of travellers, list = `GuestQuantitie`.', example: TEST }
            FirstName:       { type: string, description: '(REQUIRED) First names of travellers, list = `GuestQuantitie`.', example: TEST }
            Email:           { type: string, description: '(REQUIRED) Emails of travellers, list = `GuestQuantitie`.', example: test@test.com }
            PhoneNumber:     { type: string, example: "0606060606" }
            AddressLine:     { type: string, example: "1 rue royal" }
            CityName:        { type: string, example: "saint cloud" }
            PostalCode:      { type: string, example: "92210" }
            CountryName:     { type: string, example: FRANCE }
            CopieEmail:      { type: string, format: email, description: 'A copy of the voucher is sent to this email.' }
            LoyaltyCard:     { type: string, description: 'Loyalty card numbers (`;` or `,` separated).' }
            Login:           { type: string, description: Connected user., example: CDS }
            Comment:         { type: string, description: Comment forwarded to the property when supported.}
            ResGlobalInfo:
              type: array
              items: { type: string, example: "Agent:julie" }
              description: |
                Free-form key/value strings appended to the booking. Each
                entry follows `key:value`.
            IsTestBooking:
              type: boolean
              default: false
              description: When `true`, runs a mock booking (UAT only). The returned `RsvCd` is prefixed `MOCK-`.

    # =========================================================================
    # CANCEL / READ / SYNC
    # =========================================================================

    CdsCancelModel:
      allOf:
        - $ref: '#/components/schemas/CdsApiBase'
        - type: object
          required: [codeBooking]
          properties:
            codeBooking:
              type: string
              description: CDS Groupe booking code(s). Multiple separated by `;` or `,`.
              example: "RSV1234567"
            login:
              type: string
              description: Login of the connected user.
            reason:
              type: string
              description: Reason for the cancellation.
              default: ""
            email:
              type: string
              format: email
              description: (Internal) email override for the cancellation notice.

    CdsReadModel:
      allOf:
        - $ref: '#/components/schemas/CdsApiBase'
        - type: object
          properties:
            codeBooking:
              type: string
              description: '`RsvCd` of booking(s). Multiple separated by `;` or `,`.'
            login:
              type: string
              description: Login of the connected user.
            ville:
              type: string
              description: City of the booking.
            booker:
              type: string
            hotel:
              type: string
              description: Property name.
            traveler:
              type: string
              description: Traveller name.
            typeDate:
              type: integer
              enum: [1, 2, 3]
              default: 1
              description: '`1` Check-in, `2` Check-out, `3` Booking date.'
            start:
              type: string
              format: date
              description: Date range start (`yyyy-MM-dd`).
            end:
              type: string
              format: date
            status:
              type: [integer, "null"]
              description: '`ReservationStatus` value to filter on — see endpoint description for the supported subset.'
            cmpids:
              type: string
              description: List of company ids (`;` or `,` separated).
            araids:
              type: string
              description: List of agent ids (`;` or `,` separated).
            categoryOperation:
              type: [integer, "null"]
              description: Operational contact category enum (internal).

    CdsSyncModel:
      allOf:
        - $ref: '#/components/schemas/CdsApiBase'
        - type: object
          required: [codeBooking, login]
          properties:
            codeBooking:
              type: string
              description: '`RsvCd` of booking(s) to sync. Multiple separated by `;` or `,`.'
              example: "RSV1234567"
            login:
              type: string
              description: Login of the connected user.

    # =========================================================================
    # BOOK / CANCEL / READ / SYNC responses
    # =========================================================================

    ApiBookingResult:
      allOf:
        - $ref: '#/components/schemas/ResponseEnvelope'
        - type: object
          properties:
            RsvCdGroupCode:
              type: string
              description: CDS PNR (only serialized when non-empty).
            Bookings:
              type: array
              items: { $ref: '#/components/schemas/ApiBooking' }

    ApiBookingCancelResult:
      allOf:
        - $ref: '#/components/schemas/ResponseEnvelope'
        - type: object
          properties:
            Booking:
              $ref: '#/components/schemas/ApiBooking'

    ApiBookingSyncResult:
      allOf:
        - $ref: '#/components/schemas/ResponseEnvelope'
        - type: object
          properties:
            RsvCdGroupCode:
              type: string
              description: CDS PNR (only serialized when non-empty).
            Booking:
              $ref: '#/components/schemas/ApiBooking'
            Bookings:
              type: object
              additionalProperties:
                $ref: '#/components/schemas/ReservationStatus'
              description: Map of `RsvCd` → `ReservationStatus` for batched syncs.
            WarningMessage:
              type: string
            ListCurrencies:
              type: array
              items: { type: string }

    ApiBooking:
      type: object
      description: A confirmed (or attempted) booking. The full surface has 60+ fields; the most useful are typed here, the rest is available via `additionalProperties`.
      additionalProperties: true
      properties:
        RsvCd:           { type: string, description: CDS reservation code.}
        RsvCdMain:       { type: string, description: CDS reservation folder code.}
        RsvCd2:          { type: string, description: Supplier ID.}
        RsvCd3:          { type: string, description: Supplier ID 2 (when applicable).}
        HtlCd:           { type: string, description: CDS hotel code.}
        RsvStartDate:    { type: string, format: date-time }
        RsvEndDate:      { type: string, format: date-time }
        RsvStatus:       { $ref: '#/components/schemas/ReservationStatus' }
        RsvClientCmt:    { type: string, description: Guest comment.}
        RsvCrsCmt:       { type: string, description: CDS internal system comment.}
        RsvHtlCmt:       { type: string, description: Hotel comment.}
        RsvNotes:        { type: string }
        CurCd:           { type: string }
        RsvTotalPrice:   { type: number, format: decimal }
        RsvRoomPrice:    { type: number, format: decimal, description: Single room price.}
        RsvTotalTaxPrice: { type: number, format: decimal }
        RsvMealPrice:    { type: number, format: decimal, description: Total breakfast / meal price.}
        RsvExchangeRate: { type: [number, "null"], format: decimal, description: Exchange rate to local currency.}
        RsvExchangeRateToEuro: { type: [number, "null"], format: decimal }
        RsvInsDate:      { type: string, format: date-time, description: Insertion date.}
        RsvUpdDate:      { type: string, format: date-time, description: Last update date.}
        RsvBookedDate:   { type: [string, "null"], format: date-time }
        RsvCancelDate:   { type: [string, "null"], format: date-time }
        Hotel:           { type: object, additionalProperties: true, description: Hotel snapshot at booking time.}
        Booker:          { type: object, additionalProperties: true }
        Company:         { type: object, additionalProperties: true }
        HotelGuestList:  { type: array }
        RoomStayList:    { type: array }
        HistoryList:     { type: array }
        NotificationList: { type: array }
        CancellationFeesDetailsList: { type: array }
        BenefitList:     { type: array }
        FieldValues:     { type: object, additionalProperties: true }
        BreakfastIncluded: { type: boolean }
        TaxIncluded:     { type: boolean }
