GET
# Room availability & rates API

Extract comprehensive room details and listed rates from Booking.com hotels by hotel ID.

## Point of sale and display settings

Pass `market=ES` for Spain (Spanish and EUR defaults) or `market=US` for the United States (English and USD). The optional `language` and `currency` parameters override these defaults. Market is an ISO 3166-1 two-letter country code; unknown codes and conflicting `market`/`country_market` values return 400.

Legacy `country_market` remains supported and keeps English/USD defaults. Omitting both retains US targeting and English/USD. For search by URL, explicit language/currency query parameters override values embedded in the URL; explicit URL values override market defaults.

Defaults: ES es/EUR, US en-us/USD, GB en-gb/GBP, DE de/EUR, FR fr/EUR, IT it/EUR, PT pt-pt/EUR, NL nl/EUR, IE en-gb/EUR, CA en-us/CAD, AU en-gb/AUD, JP ja/JPY. Other valid countries use en-us/USD unless overridden.

`data.effective_market` reports `requested_country`, nullable `targeted_country`, `language`, and `currency`. Targeting is best-effort: `country_verified` is false and `verified_country` is null because the response does not independently verify geography. A null target means country targeting was not applied. MCP returns the same object as `effective_market`.

```
{"requested_country":"ES","targeted_country":"ES","country_verified":false,"verified_country":null,"language":"es","currency":"EUR"}
```

For prices, `total_price_value` follows the displayed gross basis and is not guaranteed to include every charge. `taxes_and_charges_value` is the included portion (gross minus net); inspect `tax_breakdown[].included` for excluded taxes.

Compare the same stay, occupancy, room, rate conditions and currency. Market can change tax inclusion and displayed totals without changing the all-in price; it does not guarantee a discount or availability. In the verified Spain/US comparison, the same rate displayed US$455 including taxes in Spain and $341 excluding taxes in the US, with the same $455.46 all-in amount.

## Overview

#### Live data

Returns normalized live room inventory and rate data.

The Room availability and rates endpoint returns static per-room information (descriptions, photos, bed configurations, highlights) alongside the listed rates available for a specific date range (prices, refund policies, meal plans, availability). For a catalog independent of dates and occupancy, use the [Room types endpoint](/docs/endpoints/booking/room-types).

### Endpoint URL

GET https://api.stayapi.com/v1/booking/hotel/rooms 

### Need a hotel ID?

Use [/v1/booking/hotel/url-to-id](/docs/endpoints/booking/url-to-id) to convert a Booking.com URL or slug to a numeric hotel ID.

## Query Parameters

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| hotel\_id | integer (\> 0) | Required | Numeric Booking.com hotel ID (for example, `346648`). |
| checkin | string (YYYY-MM-DD) | Optional | Arrival date. Defaults to today + 14 days. |
| checkout | string (YYYY-MM-DD) | Optional | Departure date. Defaults to today + 15 days. |
| adults | integer | Optional | Number of adults (1–30, default: 2). |
| children | integer | Optional | Number of children (0–10, default: 0). |
| rooms | integer | Optional | Number of rooms (1–8, default: 1). |
| currency | string (ISO 4217) | Optional | Display currency override (market default, otherwise `USD`). Examples: `EUR`, `GBP`, `THB`. |
| country\_market | string (ISO-3166) | Optional | Legacy two-letter country target (default: `US`), preserving English/USD defaults. It must match `market` if both are supplied. The echo is a request value, not verified geography. |
| children\_ages | string | Optional | Comma-separated integer ages 0–17, for example 5,8. Required when children is positive; the age count must equal children. Omit or leave empty for zero children. |

## Response Structure

### Top-level fields

- `success` — boolean, always `true` for 2xx responses.
- `data.hotel_info` — basic property info (`id`, `currency_code`).
- `data.rooms` — array of room _types_ with static data (see below).
- `data.rates` — array of listed blocks for the requested dates (see below).
- `data.availability_summary` — deduplicated inventory totals across `data.rates`: 
  - `availability_status` — `available` when at least one listed rate was returned, otherwise `no_inventory`.
  - `total_available_rooms` — sum of the _maximum_ `room_count_available` per `room_id`, so a room type sold under several rate plans is counted once. `null` when there are no listed rates.
  - `available_rate_blocks` — number of entries in `data.rates`.
  - `available_room_types_counted` — number of distinct `room_id`s that contributed a count.
  - `room_counts_by_room_id` — the per-room-type counts that were summed.
  - `rates_without_room_count_available` — rate blocks returned without a usable count.
  - `calculation_method` — human-readable formula string.

- `data.search_config` — echoes the guest/date configuration you requested.
- `data.metadata` — `hotel_id`, `total_rooms`, `total_blocks`, `arrival_date`, `departure_date`.
- `retrieved_at` — ISO-8601 UTC timestamp.

### Per-room fields (`data.rooms[]`)

- `id` — stable room-type identifier.
- `name` — human-readable room-type name (derived from the first associated rate).
- `description` — full room description (may contain HTML formatting).
- `ai_description` — short AI-generated comparative description (may contain `<b>` tags).
- `room_size_m2`, `room_size_ft2` — room size in metric and imperial units.
- `is_smoking`, `is_dormitory` — booleans.
- `bathroom_count` — number of bathrooms; `null` when Booking.com doesn't list a count (common for hotel rooms). `private_bathroom` — `true` for a private bathroom, `false` for a shared one, `null` when unknown.
- `photos[]` — array with `id`, `url_original`, `url_max300`, `url_square60`, `url_640x200`, and `ratio`.
- `bed_configurations[]` — each has a `beds` array with `bed_type`, `name`, `name_with_count`, `count`, `description`, `description_imperial`.
- `highlights[]` — each has `id`, `name`, `icon`.

### Per-rate fields (`data.rates[]`)

Each entry represents one listed "block" — a specific combination of a room type, a cancellation/refund policy, and a meal plan. A single room type can appear multiple times (for example, refundable vs non-refundable).

- `block_id` — unique identifier for this listed option.
- `room_id` — links this rate to an entry in `data.rooms[]`.
- `name` — full rate name, e.g. _"Laguna Twin Room - Non-refundable"_.
- `room_name` — the room-type name without the policy suffix.
- `max_occupancy`, `nr_adults`, `nr_children`.
- `refundable`, `refundable_until` — cancellation policy. cancellation\_policy\_title is a localized plain-text policy summary, or null when unavailable. cancellation\_deadline\_date is the property-local calendar date of the free-cancellation deadline in YYYY-MM-DD format, or null when unavailable or not applicable. It does not specify an exact cutoff time or time zone and is not a promise that the whole day is refundable. Refer to the rate policy for conditions. refundable\_until remains an exact timestamp or null; cancellation\_policy is unchanged.
- `breakfast_included`, `half_board`, `full_board`, `all_inclusive`, `mealplan` — meal configuration.
- `room_count_available` — how many rooms of this type Booking.com will sell for the requested dates. It is bounded by the property’s allotment on Booking.com, not its physical inventory, and is typically identical across rate blocks sharing the same `room_id`, so do not sum it across rate blocks. Use `data.availability_summary.total_available_rooms` for a deduplicated total, and compare values only within one property across dates.
- `genius_discount_percentage` — Booking.com Genius loyalty discount if applicable.
- `price` — object with `total`, `per_night`, `currency`, `formatted`, plus `before_discount` (Booking.com's struck-through price, e.g. `"$642"`) and `discount_name` (e.g. `"Bonus savings"`) when the property offers a discount, otherwise `null`. Booking.com occasionally returns a rate without the discount, so a higher price with a null `before_discount` is an undiscounted quote.

## Usage Tips

#### Static vs. dynamic data

rooms[] changes rarely (new photos, re-categorisation) — safe to cache for hours or days. rates[] is dynamic (availability, pricing) — refresh per user query.

### Best Practices

- Join `rates[].room_id` back to `rooms[].id` to display photos next to each listed rate.
- The hotel name may be unavailable here — call [/v1/booking/hotel/details](/docs/endpoints/booking/hotel-details) if you need it.
- Empty `rooms` and `rates` arrays with a 200 status mean the property has no availability for the requested dates — try different dates rather than retrying.
- If you only have a URL, call `/v1/booking/hotel/url-to-id` once and cache the resulting `hotel_id`.
- To override the market currency, pass the `currency` parameter — `hotel_info.currency_code` is the property's native currency when known, otherwise null. Use `rates[].price.currency` for every quoted amount.

## Error handling and status codes

**Status codes:** 200 on success; errors use non-2xx responses with RFC 7807 Problem Details.

- **422** : Missing or invalid `hotel_id` (not a positive integer).
- **404** `NOT_FOUND`: Upstream reports the hotel does not exist.
- **502** `UPSTREAM_ERROR`: Upstream non-2xx or protocol error.
- **503** `SERVICE_UNAVAILABLE`: Upstream WAF challenge or maintenance.
- **504** `GATEWAY_TIMEOUT`: Upstream response timed out.
- **500** `INTERNAL_ERROR`: Unexpected processing error.

### Error responses (Problem Details)

All Booking.com hotel endpoints return errors using RFC 7807 Problem Details.

```
{
  "type": "https://api.stayapi.com/errors/invalid-url",
  "title": "Invalid URL",
  "status": 400,
  "detail": "Parameter 'url' must be a full Booking.com hotel URL of the form https://www.booking.com/hotel/{cc}/{slug}.html.",
  "error_code": "INVALID_URL",
  "provided": "baan-coconut"
}
```

**Media type:** `application/problem+json`

**Standard fields:** `type`, `title`, `status`, `detail`

**Extensions:** `error_code` and any endpoint-specific fields

**2xx policy:** 2xx is returned only when data extraction succeeds.

## Family occupancy

For two adults and a child aged eight, send `adults=2&children=1&children_ages=8`. Multiple ages use a single CSV value, for example `children=2&children_ages=5%2C8`. Missing ages, count mismatches, noninteger ages and ages outside 0–17 return HTTP 400 Problem Details with `error_code: INVALID_OCCUPANCY`. Invalid count types or counts outside 0–10 return HTTP 422. Adults-only requests can omit both child parameters.

Each `data.rates[]` includes `fits_requested_occupancy`: true means that one rate accommodates the entire requested party, false means it does not, and null means the fit is unknown. Room alternatives may accommodate only part of your party. Desktop-listed family and multiroom offers report null because no guest allocation is verified. The prices summary selects a verified full-family rate; if none can be verified, prices returns HTTP 502 instead of a misleading family quote. Prices describe one room for the stay, not a combined multiroom reservation total. The rooms parameter does not establish an allocation of guests across rooms.

## Recommended offers

When `data.inventory_scope` is `recommended`, the response contains a limited selection of recommended offers. Counts and summaries describe only the returned offers, not complete property inventory. `desktop_listed` identifies the returned desktop offer selection, without a guarantee of complete inventory or bookability. The field may be omitted for other responses. If offers cannot be obtained reliably, the API returns a non-2xx Problem Details error, including `502 UPSTREAM_ERROR`, rather than a false empty result.

Use the quoted amounts and tax fields as returned. Do not calculate an anonymous public price from a Genius percentage or a strikethrough amount.

| Field | Meaning |
| --- | --- |
| `data.inventory_scope` | Optional string: `recommended` or `desktop_listed`. Counts cover returned offers only. |
| `data.rates[].price.requires_sign_in` | Boolean or null: true for an observed sign-in condition; null when unknown. |

## Quote taxes and missing values

When supplied, `price.tax_basis` is `included`, `excluded`, `mixed`, or `unknown`. `price.taxes_included` is boolean or null. `charges_included` and `charges_excluded` are null or objects with nullable `amount`, `formatted`, `currency`, and `item_type`. Preserve these amounts separately; do not add included charges again. Unknown taxes are not zero taxes.

Room facilities are available from [the static room-type catalog](/docs/endpoints/booking/room-types), not `rooms[]`. Unobserved cancellation, meal, room, and source-date values can be null; null cancellation is not non-refundable. Requested dates remain in `search_config`. `occupancy_total`, when present, is the priced guest count, without an inferred adult/child split.

## Rate labels

`each data.rates[].price object` includes `labels` (an array of readable strings), `genius_discount_pct` (a number or null), and `is_mobile_rate` (a boolean). Labels describe the quoted rate, for example `Mobile rate` or `Bonus savings`. Unlabelled rates return `[]`.

Quotes can carry sign-in conditions. `requires_sign_in: true` identifies an explicitly observed sign-in condition; `null` means eligibility is unknown, not a public-rate guarantee. Genius and member labels alone do not establish a sign-in requirement or prove a discount percentage. A null percentage means no rate-specific percentage was observed; `is_mobile_rate: false` means no explicit mobile-rate marker was observed. Labels can vary by market and occupancy and do not guarantee eligibility for logged-in member prices. Existing discount fields remain available.

Request

```curl
curl -X GET "https://api.stayapi.com/v1/booking/hotel/rooms?hotel_id=346648&checkin=2026-05-05&checkout=2026-05-06&adults=2" \
  -H "x-api-key: YOUR_API_KEY"
```

Response

```json
{
  "success": true,
  "url": "hotel_id:346648",
  "data": {
    "hotel_info": {
      "id": "346648",
      "currency_code": "THB"
    },
    "rooms": [
      {
        "id": "34664813",
        "name": "One-bedroom Loft Suite",
        "description": "Offering double-height ceilings, the split-level loft overlooks the lagoon. It features a coffee machine, safe and flat-screen TV.",
        "ai_description": "<b>Double the size</b> with <b>outdoor furniture</b>, <b>patio</b>, and a <b>walk-in shower</b>.",
        "room_size_m2": 88,
        "room_size_ft2": 947.22,
        "is_smoking": false,
        "is_dormitory": false,
        "bathroom_count": 1,
        "private_bathroom": true,
        "photos": [
          {
            "id": 263386204,
            "url_original": "https://cf.bstatic.com/xdata/images/hotel/max500/263386204.jpg?k=...",
            "url_max300": "https://cf.bstatic.com/xdata/images/hotel/max300/263386204.jpg?k=...",
            "url_square60":"https://cf.bstatic.com/xdata/images/hotel/square60/263386204.jpg?k=...",
            "url_640x200": "https://cf.bstatic.com/xdata/images/hotel/640x200/263386204.jpg?k=...",
            "ratio": 1.5
          }
        ],
        "bed_configurations": [
          {
            "beds": [
              { "bed_type": 6, "name": "Queen bed(s)", "name_with_count": "1 queen bed", "count": 1, "description": "151-180 cm wide", "description_imperial": "60-70 inches wide" }
            ]
          }
        ],
        "highlights": [
          { "id": null, "name": "Free WiFi", "icon": "bui_wifi" },
          { "id": 11, "name": "Air conditioning", "icon": "bui_weather_snowflake" }
        ]
      }
    ],
    "rates": [
      {
        "block_id": "34664883_88879319_2_2_0_600415",
        "room_id": "34664883",
        "name": "Laguna Twin Room - Non-refundable",
        "room_name": "Laguna Twin Room",
        "max_occupancy": 2,
        "nr_adults": 2,
        "nr_children": 0,
        "refundable": false,
        "refundable_until": null,
        "cancellation_deadline_date": null,
        "cancellation_policy_title": "Non-refundable",
        "breakfast_included": false,
        "half_board": false,
        "full_board": false,
        "all_inclusive": false,
        "mealplan": "Continental breakfast costs US$28 per person per night.",
        "room_count_available": 20,
        "genius_discount_percentage": 0,
        "fits_requested_occupancy": true,
        "price": {
          "total": 137.56,
          "per_night": 137.56,
          "currency": "USD",
          "formatted": "US$138",
          "before_discount": null,
          "discount_name": null,
          "labels": [],
          "genius_discount_pct": null,
          "requires_sign_in": null,
          "is_mobile_rate": false
        }
      }
    ],
    "availability_summary": {
      "availability_status": "available",
      "total_available_rooms": 20,
      "available_rate_blocks": 1,
      "available_room_types_counted": 1,
      "room_counts_by_room_id": { "34664883": 20 },
      "rates_without_room_count_available": 0,
      "calculation_method": "sum(max(room_count_available) grouped by room_id across bookable rate blocks)"
    },
    "search_config": {
      "nbRooms": 1,
      "nbAdults": 2,
      "nbChildren": 0,
      "childrenAges": [],
      "checkin": "2026-05-05",
      "checkout": "2026-05-06"
    },
    "metadata": {
      "hotel_id": "346648",
      "total_rooms": 6,
      "total_blocks": 24,
      "arrival_date": "2026-05-05",
      "departure_date": "2026-05-06"
    }
  },
  "message": "Successfully retrieved room details",
  "retrieved_at": "2026-04-21T11:06:59Z"
}
```

```json
{
  "type": "https://api.stayapi.com/errors/not-found",
  "title": "Hotel Not Found",
  "status": 404,
  "detail": "hotelPage: Hotel with id 999999999 was not found.",
  "error_code": "NOT_FOUND",
  "provider": "booking",
  "hotel_id": 999999999
}
```
