{
  "openapi": "3.0.0",
  "info": {
    "title": "Hotels.nl Hotel API",
    "version": "1.0.0",
    "description": "Search for hotels, retrieve hotel details, check availability, book rooms, manage bookings, and cancel reservations. All endpoints accept POST requests with a JSON body. The typical flow is: SEARCH → HOTEL DETAILS → BOOK. After booking: BOOKING OVERVIEW to list bookings, RETRIEVE BOOKING for details, CANCEL to cancel.",
    "contact": {
      "name": "Hotels.nl Support",
      "email": "info@hotels.nl"
    }
  },
  "servers": [
    {
      "url": "https://hotels.nl/api",
      "description": "Production"
    }
  ],
  "x-rate-limits": {
    "description": "Rate limits apply to search.php and hotel.php only. Other endpoints are NOT rate-limited.",
    "search.php": {
      "per_minute": 5,
      "per_day": 200,
      "scope": "per IP address, per endpoint"
    },
    "hotel.php": {
      "per_minute": 5,
      "per_day": 200,
      "scope": "per IP address, per endpoint"
    },
    "increase_quota": "Contact info@hotels.nl with your API key and use case to request a higher daily limit."
  },
  "x-agent-notes": [
    "Dates must be in the future. Check-out must be after check-in. Maximum stay is 30 nights.",
    "Present results by summarizing hotel names, star ratings, prices, and amenities. Do not dump raw JSON.",
    "When comparing prices, use total_price from rate.pricing. The average_per_night field (in hotel.php) is useful for nightly comparisons. Taxes (like city tax) may be extra — check the taxes array.",
    "Free cancellation info is in rate.cancellation.free_cancellation_before. null means non-refundable.",
    "Identical searches within 1 hour are served from cache and return instantly.",
    "Hotel static data is cached for 30 days. Availability results from identical requests are cached for 30 minutes.",
    "Only nomeal and breakfast meal plans are available. No half-board or all-inclusive.",
    "Bookings support 1–5 persons in a single room. Default is 2.",
    "If a request times out, wait a moment and retry once. The API has a 5-second timeout.",
    "Prices are wholesale rates, consistently lower than Booking.com and similar platforms.",
    "The search_id is valid for 1 hour. After that, search again.",
    "You CANNOT look up a hotel by name on hotel.php. Always start with search.php.",
    "Search returns a maximum of 15 hotels. Use a specific location for more relevant results.",
    "The hotelsnl_hash is a deterministic string encoding hotel, room, meal, refundability, dates, and persons. Pass it to booking.php as-is.",
    "Before attempting cancellation, ALWAYS check booking details with retrieve_booking.php first.",
    "When a user asks to cancel, confirm with them before calling cancel.php. Cancellation is irreversible.",
    "ALWAYS ask the user about preferences (stars, facilities, breakfast, budget) BEFORE searching.",
    "Amenities are returned by default in search results. Use them proactively to match user preferences.",
    "The apikey is always sent inside the JSON request body, not as a header."
  ],
  "tags": [
    { "name": "Search", "description": "Find hotels by location and availability" },
    { "name": "Hotels", "description": "Retrieve detailed hotel information and room rates" },
    { "name": "Bookings", "description": "Create, list, retrieve, and cancel reservations" }
  ],
  "paths": {
    "/search.php": {
      "post": {
        "operationId": "searchHotels",
        "summary": "Search hotels by location",
        "description": "Search for hotels near a location. Returns a list of hotels with the cheapest available rate per hotel. Only one rate (the lowest price) is returned per hotel. To see all available rooms and rates, use hotel.php. Only \"nomeal\" and \"breakfast\" meal plans are available. Rate-limited: 5 requests/minute, 200 requests/day per IP.",
        "tags": ["Search"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["apikey", "checkin", "checkout"],
                "properties": {
                  "apikey": {
                    "type": "string",
                    "description": "Your API key. Always required."
                  },
                  "checkin": {
                    "type": "string",
                    "format": "date",
                    "description": "Check-in date. Format: YYYY-MM-DD.",
                    "example": "2026-06-15"
                  },
                  "checkout": {
                    "type": "string",
                    "format": "date",
                    "description": "Check-out date. Format: YYYY-MM-DD.",
                    "example": "2026-06-18"
                  },
                  "location": {
                    "type": "string",
                    "description": "Any text: city name, street address, landmark, airport, point of interest. The API geocodes it automatically via Google. Required if latitude/longitude are not provided.",
                    "example": "Dam Square, Amsterdam"
                  },
                  "latitude": {
                    "type": "number",
                    "format": "float",
                    "description": "Latitude of the search center. Use together with longitude as an alternative to location (faster, skips geocoding).",
                    "example": 52.3731
                  },
                  "longitude": {
                    "type": "number",
                    "format": "float",
                    "description": "Longitude of the search center. Use together with latitude as an alternative to location.",
                    "example": 4.8932
                  },
                  "residency": {
                    "type": "string",
                    "description": "Guest country code (ISO 3166-1 alpha-2).",
                    "default": "nl",
                    "example": "nl"
                  },
                  "language": {
                    "type": "string",
                    "description": "Response language code.",
                    "default": "nl",
                    "example": "nl"
                  },
                  "currency": {
                    "type": "string",
                    "description": "ISO 4217 currency code.",
                    "default": "EUR",
                    "example": "EUR"
                  },
                  "radius": {
                    "type": "integer",
                    "description": "Search radius in meters.",
                    "minimum": 1,
                    "maximum": 10000,
                    "default": 5000,
                    "example": 5000
                  },
                  "persons": {
                    "type": "integer",
                    "description": "Number of persons for the room (single room).",
                    "minimum": 1,
                    "maximum": 5,
                    "default": 2,
                    "example": 2
                  },
                  "star_rating": {
                    "type": "array",
                    "description": "Filter by star rating. 0 = unrated.",
                    "items": {
                      "type": "integer",
                      "enum": [0, 1, 2, 3, 4, 5]
                    },
                    "example": [3, 4, 5]
                  },
                  "kind": {
                    "type": "array",
                    "description": "Filter by property type.",
                    "items": {
                      "type": "string",
                      "enum": [
                        "Hotel",
                        "Apartment",
                        "Resort",
                        "BNB",
                        "Hostel",
                        "Guesthouse",
                        "Apart-hotel",
                        "Boutique_and_Design",
                        "Camping",
                        "Castle",
                        "Cottages_and_Houses",
                        "Farm",
                        "Glamping",
                        "Mini-hotel",
                        "Sanatorium",
                        "Villas_and_Bungalows"
                      ]
                    },
                    "example": ["Hotel", "Resort"]
                  },
                  "meal_type": {
                    "type": "array",
                    "description": "Filter by meal plan. Only these two are available — no half-board or all-inclusive.",
                    "items": {
                      "type": "string",
                      "enum": ["nomeal", "breakfast"]
                    },
                    "example": ["breakfast"]
                  },
                  "price_from": {
                    "type": "integer",
                    "description": "Minimum total price filter.",
                    "example": 100
                  },
                  "price_to": {
                    "type": "integer",
                    "description": "Maximum total price filter.",
                    "example": 500
                  },
                  "include_description": {
                    "type": "boolean",
                    "description": "Set true to include short_description per hotel.",
                    "default": false
                  },
                  "include_amenities": {
                    "type": "boolean",
                    "description": "Include amenities per hotel.",
                    "default": true
                  }
                }
              },
              "example": {
                "apikey": "USER_API_KEY",
                "location": "Dam Square, Amsterdam",
                "checkin": "2026-06-15",
                "checkout": "2026-06-18",
                "star_rating": [3, 4, 5],
                "persons": 2
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful search. Returns search metadata and a list of hotels with the cheapest rate per hotel. Maximum 15 hotels.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SearchResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid parameters (e.g. missing location, missing dates, invalid date format).",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "401": {
            "description": "API key is missing.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "403": {
            "description": "API key is invalid.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "405": {
            "description": "Method not allowed — only POST accepted.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (5 requests/minute or 200 requests/day). Retry-After header indicates seconds to wait.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": { "type": "integer" }
              }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/hotel.php": {
      "post": {
        "operationId": "getHotelDetails",
        "summary": "Hotel details and availability",
        "description": "Returns detailed hotel information (description, images, amenities, policies, etc.) together with room availability and rates. You MUST provide either a search_id or checkin + checkout dates — requests without dates are rejected. The hotel id can only be obtained from a search.php response. Rate-limited: 5 requests/minute, 200 requests/day per IP.",
        "tags": ["Hotels"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["apikey", "id"],
                "properties": {
                  "apikey": {
                    "type": "string",
                    "description": "Your API key. Always required."
                  },
                  "id": {
                    "type": "integer",
                    "description": "The hotel ID from search results (hotels[].id). Always required. Cannot search by name.",
                    "example": 1584
                  },
                  "search_id": {
                    "type": "string",
                    "description": "The search_id from a previous search response. PREFERRED method — returns cached rates instantly. Valid for 1 hour.",
                    "example": "a3f8c9..."
                  },
                  "checkin": {
                    "type": "string",
                    "format": "date",
                    "description": "Check-in date (YYYY-MM-DD). Use with checkout for a fresh live availability lookup when you don't have a search_id.",
                    "example": "2026-06-15"
                  },
                  "checkout": {
                    "type": "string",
                    "format": "date",
                    "description": "Check-out date (YYYY-MM-DD). Use with checkin.",
                    "example": "2026-06-18"
                  },
                  "persons": {
                    "type": "integer",
                    "description": "Number of persons for the room. Only used with checkin/checkout.",
                    "minimum": 1,
                    "maximum": 5,
                    "default": 2,
                    "example": 2
                  },
                  "currency": {
                    "type": "string",
                    "description": "Price currency. Only used with checkin/checkout.",
                    "default": "EUR",
                    "example": "EUR"
                  },
                  "residency": {
                    "type": "string",
                    "description": "Guest country code (ISO 3166-1 alpha-2). Only used with checkin/checkout.",
                    "default": "nl",
                    "example": "nl"
                  },
                  "language": {
                    "type": "string",
                    "description": "Response language code.",
                    "default": "en",
                    "example": "en"
                  }
                }
              },
              "examples": {
                "with_search_id": {
                  "summary": "With search_id (preferred — instant)",
                  "value": {
                    "apikey": "USER_API_KEY",
                    "id": 1584,
                    "search_id": "a3f8c9..."
                  }
                },
                "fresh_lookup": {
                  "summary": "Fresh availability lookup",
                  "value": {
                    "apikey": "USER_API_KEY",
                    "id": 1584,
                    "checkin": "2026-06-15",
                    "checkout": "2026-06-18",
                    "persons": 2
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Hotel details with room availability and rates.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HotelResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid parameters, or no dates provided (search_id or checkin+checkout required).",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "401": {
            "description": "API key is missing.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "403": {
            "description": "API key is invalid.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "404": {
            "description": "Hotel not found.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "405": {
            "description": "Method not allowed — only POST accepted.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Retry-After header indicates seconds to wait.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": { "type": "integer" }
              }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/booking.php": {
      "post": {
        "operationId": "createBooking",
        "summary": "Start a booking",
        "description": "Initiates a hotel reservation for a specific rate. Send the hotelsnl_hash from any rate in a search or hotel response together with the API key. The system handles the entire booking process in the background and returns a finalization URL where the guest completes payment. Guest information is pre-filled from the registered API account. This endpoint is NOT rate-limited.",
        "tags": ["Bookings"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["apikey", "hotelsnl_hash"],
                "properties": {
                  "apikey": {
                    "type": "string",
                    "description": "Your API key. Always required."
                  },
                  "hotelsnl_hash": {
                    "type": "string",
                    "description": "The hotelsnl_hash from any rate. Encodes hotel, room type, rate type, dates, and persons. Format: {hotel_id}-{room_code}-{rate_type}-{YYYYMMDD}-{YYYYMMDD}-{persons}. Rate types: 1=non-refundable+nomeal, 2=non-refundable+breakfast, 3=refundable+nomeal, 4=refundable+breakfast.",
                    "example": "1584-a0b2c3d0e2f3g0h0i0j2k0l0-1-20260615-20260618-2"
                  }
                }
              },
              "example": {
                "apikey": "USER_API_KEY",
                "hotelsnl_hash": "1584-a0b2c3d0e2f3g0h0i0j2k0l0-1-20260615-20260618-2"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Booking initiated successfully. Returns a URL for the guest to finalize and pay.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BookingResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid hotelsnl_hash.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "401": {
            "description": "API key is missing.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "403": {
            "description": "API key is invalid.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/booking_overview.php": {
      "post": {
        "operationId": "listBookings",
        "summary": "List all bookings",
        "description": "Returns a list of all bookings for your API key. Only bookings that have progressed past initial creation are included (i.e. where a payment was attempted). Results are sorted by booking date, newest first. This endpoint is NOT rate-limited.",
        "tags": ["Bookings"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["apikey"],
                "properties": {
                  "apikey": {
                    "type": "string",
                    "description": "Your API key. Always required."
                  }
                }
              },
              "example": {
                "apikey": "USER_API_KEY"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "List of bookings.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BookingOverviewResponse"
                }
              }
            }
          },
          "401": {
            "description": "API key is missing.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "403": {
            "description": "API key is invalid.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/retrieve_booking.php": {
      "post": {
        "operationId": "retrieveBooking",
        "summary": "Retrieve booking details",
        "description": "Retrieves full details of a single booking. The booking must belong to the account associated with the API key. Returns comprehensive information including hotel details, guest data, pricing, cancellation policy, and current status. This endpoint is NOT rate-limited.",
        "tags": ["Bookings"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["apikey", "bookhash"],
                "properties": {
                  "apikey": {
                    "type": "string",
                    "description": "Your API key. Always required."
                  },
                  "bookhash": {
                    "type": "string",
                    "description": "The 30-character booking reference hash (from booking_overview or the original booking response).",
                    "example": "aBcDeFgHiJkLmNoPqRsTuVwXyZ1234"
                  }
                }
              },
              "example": {
                "apikey": "USER_API_KEY",
                "bookhash": "aBcDeFgHiJkLmNoPqRsTuVwXyZ1234"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Full booking details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RetrieveBookingResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing bookhash.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "401": {
            "description": "API key is missing.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "403": {
            "description": "API key is invalid, or the booking belongs to another account.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "404": {
            "description": "Booking not found.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/book/cancel.php": {
      "post": {
        "operationId": "cancelBooking",
        "summary": "Cancel a booking",
        "description": "Cancels a confirmed reservation. Only bookings with status \"booking_confirmed\" can be cancelled, and only within the free cancellation window. After the cancellation deadline, cancellation is no longer possible and no refund will be given. Non-refundable bookings cannot be cancelled. Always check eligibility with retrieve_booking.php first. This endpoint is NOT rate-limited.",
        "tags": ["Bookings"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["apikey", "bookhash"],
                "properties": {
                  "apikey": {
                    "type": "string",
                    "description": "Your API key. Always required."
                  },
                  "bookhash": {
                    "type": "string",
                    "description": "The 30-character booking reference hash.",
                    "example": "aBcDeFgHiJkLmNoPqRsTuVwXyZ1234"
                  }
                }
              },
              "example": {
                "apikey": "USER_API_KEY",
                "bookhash": "aBcDeFgHiJkLmNoPqRsTuVwXyZ1234"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Booking cancelled successfully. A confirmation email is sent to the guest. Payment settlement is handled within two working days.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CancelResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing bookhash.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "401": {
            "description": "API key is missing.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "403": {
            "description": "API key is invalid, cancellation deadline has passed, or rate is non-refundable.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" },
                "examples": {
                  "deadline_passed": {
                    "summary": "Cancellation deadline passed",
                    "value": { "error": "Free cancellation deadline (2026-06-02T21:59:00) has passed. Contact info@hotels.nl for assistance." }
                  },
                  "non_refundable": {
                    "summary": "Non-refundable rate",
                    "value": { "error": "Non-refundable rate — online cancellation not available. Contact info@hotels.nl for assistance." }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Booking not found.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "409": {
            "description": "Booking is not in a cancellable state (not status \"booking_confirmed\").",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" },
                "example": { "error": "This reservation has already been cancelled.", "status": "already_cancelled" }
              }
            }
          },
          "502": {
            "description": "Cancellation failed at the hotel supplier. Contact info@hotels.nl.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "ErrorResponse": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable error message."
          }
        },
        "required": ["error"]
      },
      "SearchResponse": {
        "type": "object",
        "properties": {
          "search_id": {
            "type": "string",
            "description": "Unique identifier for this search. Pass to hotel.php for instant rate lookups. Valid for 1 hour."
          },
          "search": {
            "$ref": "#/components/schemas/SearchMetadata"
          },
          "total_hotels": {
            "type": "integer",
            "description": "Number of hotels returned (max 15)."
          },
          "hotels": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/SearchHotel" }
          }
        }
      },
      "SearchMetadata": {
        "type": "object",
        "properties": {
          "checkin": { "type": "string", "format": "date" },
          "checkout": { "type": "string", "format": "date" },
          "nights": { "type": "integer" },
          "latitude": { "type": "number", "format": "float" },
          "longitude": { "type": "number", "format": "float" },
          "radius_m": { "type": "integer" },
          "currency": { "type": "string" },
          "language": { "type": "string" },
          "residency": { "type": "string" },
          "persons": { "type": "integer" }
        }
      },
      "SearchHotel": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Internal hotel ID. Use this as the id field when calling hotel.php."
          },
          "name": { "type": "string" },
          "address": { "type": "string" },
          "city": { "type": "string" },
          "country_code": { "type": "string" },
          "star_rating": {
            "type": "integer",
            "minimum": 0,
            "maximum": 5
          },
          "kind": { "type": "string", "description": "Property type (Hotel, Apartment, Resort, etc.)." },
          "latitude": { "type": "number", "format": "float" },
          "longitude": { "type": "number", "format": "float" },
          "image": { "type": "string", "format": "uri", "description": "Primary hotel image URL." },
          "amenities": {
            "type": "string",
            "description": "Comma-separated amenity names. Included by default.",
            "nullable": true
          },
          "short_description": {
            "type": "string",
            "description": "Short hotel description. Only present when include_description is true.",
            "nullable": true
          },
          "rate": { "$ref": "#/components/schemas/SearchRate" }
        }
      },
      "SearchRate": {
        "type": "object",
        "description": "The cheapest available rate for this hotel.",
        "properties": {
          "hotelsnl_hash": {
            "type": "string",
            "description": "Deterministic booking reference. Send to booking.php to start a reservation. Format: {hotel_id}-{room_code}-{rate_type}-{YYYYMMDD}-{YYYYMMDD}-{persons}."
          },
          "search_hash": {
            "type": "string",
            "description": "Internal reference used by the booking system. No action required."
          },
          "room": { "$ref": "#/components/schemas/RoomSummary" },
          "meal": {
            "type": "string",
            "enum": ["nomeal", "breakfast"],
            "description": "Meal plan."
          },
          "pricing": { "$ref": "#/components/schemas/Pricing" },
          "cancellation": { "$ref": "#/components/schemas/Cancellation" }
        }
      },
      "RoomSummary": {
        "type": "object",
        "properties": {
          "room_name": { "type": "string" },
          "room_class": { "type": "string" },
          "bedding": { "type": "string" }
        }
      },
      "Pricing": {
        "type": "object",
        "properties": {
          "total_price": {
            "type": "string",
            "description": "Total display price for the entire stay."
          },
          "currency": {
            "type": "string",
            "description": "ISO 4217 currency code."
          },
          "price_per_night": {
            "type": "array",
            "items": { "type": "string" },
            "description": "Price for each night."
          },
          "average_per_night": {
            "type": "string",
            "description": "Average nightly rate. Present in hotel.php responses.",
            "nullable": true
          },
          "charge_amount": {
            "type": "string",
            "description": "Actual charge amount (may differ from total_price for currency conversion).",
            "nullable": true
          },
          "charge_currency": {
            "type": "string",
            "description": "Currency of charge_amount.",
            "nullable": true
          },
          "taxes": {
            "type": "array",
            "description": "Extra taxes NOT included in total_price. May be empty or absent when taxes are included.",
            "items": { "$ref": "#/components/schemas/Tax" },
            "nullable": true
          },
          "vat": {
            "type": "object",
            "description": "VAT when not included in total_price. Absent when included.",
            "properties": {
              "amount": { "type": "string" },
              "currency": { "type": "string" }
            },
            "nullable": true
          }
        }
      },
      "Tax": {
        "type": "object",
        "properties": {
          "name": { "type": "string", "description": "Tax name (e.g. city_tax)." },
          "amount": { "type": "string" },
          "currency": { "type": "string" }
        }
      },
      "Cancellation": {
        "type": "object",
        "properties": {
          "free_cancellation_before": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "ISO datetime deadline for free cancellation. null means non-refundable."
          },
          "policies": {
            "type": "array",
            "description": "Penalty periods.",
            "items": { "$ref": "#/components/schemas/CancellationPolicy" }
          }
        }
      },
      "CancellationPolicy": {
        "type": "object",
        "properties": {
          "starts_at": { "type": "string", "format": "date-time" },
          "ends_at": { "type": "string", "format": "date-time" },
          "penalty_amount": { "type": "string" },
          "penalty_charge": { "type": "string" }
        }
      },
      "HotelResponse": {
        "type": "object",
        "properties": {
          "id": { "type": "integer", "description": "Internal hotel ID." },
          "name": { "type": "string" },
          "type": { "type": "string", "description": "Property type (Hotel, Apartment, Resort, etc.)." },
          "star_rating": { "type": "integer", "minimum": 0, "maximum": 5 },
          "is_closed": { "type": "boolean" },
          "hotel_chain": { "type": "string", "nullable": true },
          "location": {
            "type": "object",
            "properties": {
              "address": { "type": "string" },
              "postal_code": { "type": "string" },
              "latitude": { "type": "number", "format": "float" },
              "longitude": { "type": "number", "format": "float" },
              "city": { "type": "string" },
              "country_code": { "type": "string" },
              "region_type": { "type": "string", "nullable": true },
              "airport_iata": { "type": "string", "nullable": true },
              "distance_to_center_m": { "type": "integer", "nullable": true }
            }
          },
          "times": {
            "type": "object",
            "properties": {
              "check_in": { "type": "string" },
              "check_out": { "type": "string" },
              "front_desk_open": { "type": "string", "nullable": true },
              "front_desk_close": { "type": "string", "nullable": true }
            }
          },
          "property": {
            "type": "object",
            "properties": {
              "total_rooms": { "type": "integer", "nullable": true },
              "floors": { "type": "integer", "nullable": true },
              "year_built": { "type": "integer", "nullable": true },
              "year_renovated": { "type": "integer", "nullable": true },
              "payment_methods": { "type": "array", "items": { "type": "string" }, "nullable": true },
              "electricity": { "type": "string", "nullable": true }
            }
          },
          "descriptions": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "title": { "type": "string" },
                "text": { "type": "string" }
              }
            }
          },
          "images": {
            "type": "array",
            "description": "Hotel images (1024x768).",
            "items": {
              "type": "object",
              "properties": {
                "url": { "type": "string", "format": "uri" },
                "category": { "type": "string" }
              }
            }
          },
          "amenities": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "group": { "type": "string" },
                "included": { "type": "array", "items": { "type": "string" } },
                "paid": { "type": "array", "items": { "type": "string" } }
              }
            }
          },
          "quick_features": {
            "type": "array",
            "items": { "type": "string" },
            "description": "Quick feature names (e.g. Parking, Internet, Pool)."
          },
          "policies": {
            "type": "object",
            "description": "Hotel policies: internet, parking, meals, pets, extra_beds, cots, shuttle, children_beds, additional_fees.",
            "additionalProperties": true
          },
          "important_notes": { "type": "string", "nullable": true, "description": "Special notes from the hotel." },
          "availability": {
            "type": "object",
            "description": "Availability metadata. Present when dates are provided.",
            "nullable": true,
            "properties": {
              "source": { "type": "string", "enum": ["search_cache", "live"], "description": "Whether rates came from the cached search or a live lookup." },
              "checkin": { "type": "string", "format": "date" },
              "checkout": { "type": "string", "format": "date" },
              "nights": { "type": "integer" },
              "currency": { "type": "string" },
              "message": { "type": "string", "description": "Present when no rooms are available.", "nullable": true }
            }
          },
          "rooms": {
            "type": "array",
            "description": "Available rooms. Empty array when no rooms found.",
            "nullable": true,
            "items": { "$ref": "#/components/schemas/HotelRoom" }
          }
        }
      },
      "HotelRoom": {
        "type": "object",
        "properties": {
          "room_name": { "type": "string", "description": "Full room name." },
          "room_type": { "type": "string" },
          "room_class": { "type": "string", "description": "Room, Suite, Studio, Apartment, etc." },
          "quality": { "type": "string", "description": "Standard, Superior, Deluxe, Comfort, Economy, etc." },
          "bedding": { "type": "string", "description": "Double Bed, Twin Beds, Single, Multiple Beds, etc." },
          "bedrooms": { "type": "integer", "nullable": true, "description": "Number of bedrooms. Omitted when 0." },
          "capacity": { "type": "string", "description": "Single, Double, Triple, Quadruple, etc." },
          "bathroom": { "type": "string", "description": "Private or Shared." },
          "view": { "type": "string", "nullable": true, "description": "View type. Omitted when none." },
          "balcony": { "type": "boolean", "description": "Has balcony. Omitted when false." },
          "family_room": { "type": "boolean", "description": "Family room. Omitted when false." },
          "club_room": { "type": "boolean", "description": "Club/executive room. Omitted when false." },
          "floor": { "type": "string", "nullable": true, "description": "Floor type. Omitted when none." },
          "amenities": {
            "type": "array",
            "items": { "type": "string" },
            "description": "Room-level amenity tags."
          },
          "rates": {
            "type": "array",
            "description": "Rate options for this room (up to 4): nomeal non-refundable, nomeal refundable, breakfast non-refundable, breakfast refundable.",
            "items": { "$ref": "#/components/schemas/HotelRate" }
          }
        }
      },
      "HotelRate": {
        "type": "object",
        "properties": {
          "hotelsnl_hash": {
            "type": "string",
            "description": "Deterministic booking reference. Send to booking.php."
          },
          "search_hash": {
            "type": "string",
            "description": "Internal reference. No action required."
          },
          "meal": {
            "type": "string",
            "enum": ["nomeal", "breakfast"]
          },
          "refundable": {
            "type": "boolean",
            "description": "true = free cancellation available before deadline, false = non-refundable."
          },
          "pricing": { "$ref": "#/components/schemas/Pricing" },
          "cancellation": { "$ref": "#/components/schemas/Cancellation" },
          "booking": {
            "type": "object",
            "properties": {
              "rooms_available": { "type": "integer" },
              "any_residency": { "type": "boolean" },
              "is_package_rate": { "type": "boolean" }
            }
          }
        }
      },
      "BookingResponse": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": ["ok"],
            "description": "ok when the booking was successfully initiated."
          },
          "booking_url": {
            "type": "string",
            "format": "uri",
            "description": "URL to the finalization page where the guest reviews the booking summary and completes payment with one click. All guest details are pre-filled."
          }
        }
      },
      "BookingOverviewResponse": {
        "type": "object",
        "properties": {
          "status": { "type": "string", "enum": ["ok"] },
          "count": { "type": "integer", "description": "Total number of bookings returned." },
          "bookings": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/BookingOverviewItem" }
          }
        }
      },
      "BookingOverviewItem": {
        "type": "object",
        "properties": {
          "booking_id": { "type": "integer", "description": "Hotels.nl internal booking ID." },
          "bookhash": { "type": "string", "description": "Unique 30-character booking reference. Use for retrieve_booking or cancel." },
          "booked_at": { "type": "string", "format": "date-time", "description": "When the booking was created." },
          "arrival": { "type": "string", "format": "date" },
          "departure": { "type": "string", "format": "date" },
          "guest_name": { "type": "string", "description": "Guest full name (first + last)." },
          "hotel_name": { "type": "string" },
          "room_name": { "type": "string" },
          "status": {
            "type": "string",
            "enum": ["payment_failed", "booking_failed", "booking_confirmed", "booking_cancelled"],
            "description": "Current booking status."
          }
        }
      },
      "RetrieveBookingResponse": {
        "type": "object",
        "properties": {
          "status": { "type": "string", "enum": ["ok"] },
          "booking": { "$ref": "#/components/schemas/BookingDetail" }
        }
      },
      "BookingDetail": {
        "type": "object",
        "properties": {
          "id": { "type": "integer", "description": "Hotels.nl internal booking ID." },
          "arrival": { "type": "string", "format": "date" },
          "departure": { "type": "string", "format": "date" },
          "persons": { "type": "integer" },
          "first_name": { "type": "string" },
          "last_name": { "type": "string" },
          "email": { "type": "string", "format": "email" },
          "phone": { "type": "string" },
          "client_remarks": { "type": "string", "nullable": true, "description": "Guest remarks entered during booking." },
          "bookhash": { "type": "string" },
          "room_name": { "type": "string" },
          "bedding": { "type": "string" },
          "meal": { "type": "string", "enum": ["nomeal", "breakfast"] },
          "total_price": { "type": "string" },
          "currency": { "type": "string" },
          "refundable": { "type": "boolean", "description": "true if free cancellation is available." },
          "free_cancellation_before": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "ISO datetime deadline for free cancellation. null if non-refundable."
          },
          "status": {
            "type": "string",
            "enum": ["new", "processing", "booking_failed", "booking_confirmed", "booking_cancelled"],
            "description": "Current booking status."
          },
          "paid": { "type": "string", "description": "Payment status (e.g. yes, no, refunded)." },
          "ratehawk_order_id": { "type": "string", "nullable": true, "description": "Supplier order reference." },
          "hotel_name": { "type": "string" },
          "hotel_address": { "type": "string" },
          "hotel_city": { "type": "string" },
          "hotel_country": { "type": "string" },
          "hotel_stars": { "type": "integer" },
          "created_at": { "type": "string", "format": "date-time" }
        }
      },
      "CancelResponse": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": ["cancelled"],
            "description": "Cancellation was successful."
          },
          "booking_id": { "type": "integer" },
          "bookhash": { "type": "string" }
        }
      }
    }
  }
}
