# Booking.com Hotels Scraper - Dated Prices & Rooms (`diopside/booking-hotels`) Actor

Booking.com hotel search with dated pricing: per-night and total price, taxes and charges, price before discount, and every matched room offer with its meal plan and free-cancellation deadline. Drop-in compatible with voyager/booking-scraper. Filters are verified against Booking's echo.

- **URL**: https://apify.com/diopside/booking-hotels.md
- **Developed by:** [DIOPSIDE AI](https://apify.com/diopside) (community)
- **Categories:** Travel, E-commerce, Agents
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 hotel records

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Booking.com Hotels Scraper

**Drop-in compatible with `voyager/booking-scraper` — cheaper, and room data is included instead of
being an add-on.**

Search [Booking.com](https://www.booking.com) by destination, dates and occupancy and get back every
property with the price for **that exact stay**: per-night price, total, taxes and charges, the
struck-through price when there is a discount, and every room offer Booking matched to your party —
room type, bed layout, meal plan, and the free-cancellation deadline.

Public search-result data only. No login, no guest names, no personal data.

### Why this one

**1. Prices are numbers, not parsed strings.** Booking renders prices localised (`US$1,118`), and a
scraper that reads the rendered text has to guess whether `,` is a thousands separator or a decimal
point. This actor reads Booking's own numeric field (`amountUnformatted`), so `US$1,118` is `1118.0`
and never `1.118`. `currency` is an ISO 4217 code (`USD`), not a symbol (`US$`).

**2. Room offers are part of the record, at no extra charge.** The incumbent puts surroundings and
room offerings behind a second `additional-hotel-data-scraped` event, which doubles the price of a
complete record. Here `rooms[]` ships with every hotel: room type, bed layout, per-offer price in
both your currency and the hotel's, meal plan, "only 1 left at this price", and
`freeCancellationUntil` as a real timestamp.

**3. `&offset=` on Booking's search page is a no-op.** The server-rendered results page returns the
same first ~18 properties for `offset=0`, `offset=25` and `offset=50` — with a `200 OK` and no hint
that anything was ignored. Real pagination only happens through Booking's `FullSearch` GraphQL
operation. This actor paginates there and de-duplicates by property id, so asking for 200 hotels
gets you 200 distinct hotels.

**4. The bot wall is handled once per run, not once per page.** Booking fronts every page with an
AWS WAF JavaScript challenge — plain `httpx` and `curl_cffi` impersonating Chrome both get the
interstitial. A real browser is launched **once**, clears the challenge, and hands its `aws-waf-token`
to ordinary HTTP requests for the rest of the run. Nothing about Booking's GraphQL query is
hard-coded: the browser is made to fire one `FullSearch` call and that request is captured and
replayed, so a front-end release that rewrites the query or rotates the CSRF token does not break
the actor.

**5. Filters are verified, not assumed.** Booking answers `200 OK` with a plausible *unfiltered*
feed when a filter id is stale or wrong. After the first page of every destination, this actor
re-reads the filter list Booking echoes back and fails that destination loudly if something was
dropped — rather than writing unfiltered rows into your dataset. `propertyType` is resolved against
the category list Booking returns for *your* destination, so a renamed category is an error, not a
silent no-op.

#### vs `voyager/booking-scraper`

The incumbent is healthy (4.5★, ~0.9% failed runs) — this is not a displacement play, it is a
cheaper and more complete record:

| | `voyager/booking-scraper` | this actor |
| --- | --- | --- |
| Price per property | $0.005 | **$0.003** |
| Room offers / surroundings | +$0.005 add-on event | included |
| Complete record | $0.010 | **$0.003** |
| `currency` | symbol (`US$`) | ISO code (`USD`) |
| Price parsing | from rendered text | Booking's numeric field |
| Taxes & charges, price before discount | — | yes |
| `freeCancellationUntil` timestamp | — | yes |
| Children priced by age | — | `childrenAges` |
| Property `description` | from the property page | not returned (search cards do not carry one) |

#### Switching from `voyager/booking-scraper`

Change the actor id. Every input field name (`search`, `startUrls`, `maxItems`, `propertyType`,
`sortBy`, `minScore`, `starsCountFilter`, `currency`, `language`, `checkIn`, `checkOut`,
`flexWindow`, `rooms`, `adults`, `children`, `minMaxPrice`) and the core output field names
(`order`, `url`, `name`, `type`, `id`, `description`, `stars`, `price`, `currency`, `rating`,
`ratingLabel`, `reviews`, `breakfast`, `location`, `address`, `image`, `rooms`) mean the same thing.
Everything this actor adds sits alongside them under new names.

Two differences to know about:

- `currency` is the ISO code instead of the symbol. If you were displaying it verbatim, map it.
- `description` is `null`. Booking's search cards do not carry a property description; the incumbent
  fills it by fetching each property page, which is what its add-on event pays for.

### Input

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `search` | string | — | Destination: city, region, landmark or address. |
| `startUrls` | array | `[]` | Booking URLs: `/searchresults.html?ss=…`, `?dest_id=…&dest_type=city`, `/city/nl/amsterdam.html`, or a single `/hotel/nl/….html`. |
| `maxItems` | integer | `25` | Properties per destination or URL. |
| `checkIn` / `checkOut` | `YYYY-MM-DD` | 1 night, 30 days out | The stay the price is for. |
| `flexWindow` | `"0"`–`"7"` | `"0"` | Let Booking shift the stay by up to N days. |
| `rooms` / `adults` / `children` | integer | `1` / `2` / `0` | Occupancy the price covers. |
| `childrenAges` | array of integers | `[]` | One age (0–17) per child. Booking prices children by age. |
| `propertyType` | enum | `"none"` | `Hotels`, `Apartments`, `Hostels`, `Resorts`, `Villas`, … |
| `sortBy` | enum | `"distance_from_search"` | Also `popularity`, `price`, `price_from_high_to_low`, `bayesian_review_score`, `class_asc`, `class_descending`, … |
| `minScore` | string | — | Minimum guest score, `"8.4"` format (rounded down to Booking's 5.0/6.0/7.0/8.0/9.0 buckets). |
| `starsCountFilter` | enum | `"any"` | `1`–`5`, or `unrated`. |
| `minMaxPrice` | string | `"0-999999"` | Price **per night**, e.g. `"100-150"` or `"100+"`. |
| `currency` | ISO 4217 | `"USD"` | Currency every price is returned in. |
| `language` | string | `"en-gb"` | Language Booking renders names and labels in. |
| `proxyConfiguration` | object | Apify proxy | See *Proxy* below. |

### Output

One record per property. Example (trimmed):

```json
{
  "order": 0,
  "url": "https://www.booking.com/hotel/nl/twentyseven-amsterdam.en-gb.html?checkin=2026-11-10&checkout=2026-11-12&group_adults=2&no_rooms=1&group_children=0&selected_currency=USD",
  "name": "Hotel TwentySeven - Small Luxury Hotels of the World",
  "type": "hotel",
  "id": 2785940,
  "stars": 5.0,
  "price": 1637.61,
  "currency": "USD",
  "rating": 9.4,
  "ratingLabel": "Superb",
  "reviews": 213,
  "breakfast": null,
  "location": { "lat": 52.3724914, "lng": 4.89332549999995 },
  "address": {
    "full": "27 Dam, Amsterdam",
    "street": "27 Dam",
    "country": "NL",
    "region": "Amsterdam City Centre, Amsterdam",
    "postalCode": null
  },
  "image": "https://cf.bstatic.com/xdata/images/hotel/square600/559981856.webp",
  "rooms": [
    {
      "available": true,
      "roomType": "Junior Suite",
      "bedType": "1 large double bed",
      "persons": 2,
      "price": 1637.61,
      "currency": "USD",
      "priceInHotelCurrency": 1428.73,
      "hotelCurrency": "EUR",
      "freeCancellationUntil": null,
      "onlyXLeftMessage": null
    }
  ],
  "checkInDate": "2026-11-10",
  "checkOutDate": "2026-11-12",
  "nights": 2,
  "adults": 2,
  "children": 0,
  "roomsRequested": 1,
  "pricePerNight": 818.81,
  "priceTotal": 1637.61,
  "priceBeforeDiscount": null,
  "taxesAndCharges": 472.33,
  "city": "Amsterdam",
  "countryCode": "nl",
  "latitude": 52.3724914,
  "longitude": 4.89332549999995,
  "distanceFromCentre": "150 feet from centre",
  "publicTransport": "Rokin station is within 250 yards",
  "isCentrallyLocated": true,
  "freeCancellation": false,
  "noPrepayment": false,
  "isSoldOut": false,
  "isSustainable": false,
  "sustainabilityCertifications": [],
  "isPreferred": true,
  "isSponsored": false,
  "searchLabel": "Amsterdam",
  "searchUrl": "https://www.booking.com/searchresults.html?ss=Amsterdam&checkin=2026-11-10&checkout=2026-11-12&group_adults=2&no_rooms=1&group_children=0&selected_currency=USD&lang=en-gb",
  "scrapedAt": "2026-09-21T06:50:40.678556+00:00"
}
```

Missing upstream values are `null`, never a crash. `bedType` names the five bed types Booking's own
cards label; an id Booking has not shown us reads as a plain `"bed"` rather than a guess.

### Sizes and limits

- **Per destination:** Booking serves roughly 1,000 results for one search before it stops returning
  new properties. When that ceiling — and not `maxItems` — is what stopped a destination, the run
  says so in its status message. To go deeper, split the search (by star rating, price band or
  district) into several start URLs.
- **Speed:** one browser launch (~15–25 s) plus ~1 s per page of 25 properties.
- **Prices move.** Every record carries `scrapedAt` and the stay it was priced for, so a series of
  runs is directly comparable.

### Use cases

- **Rate shopping / revenue management** — track a compset's nightly rate for a rolling date window.
- **Travel meta-search and price alerts** — per-night and total price with taxes broken out.
- **Market analysis** — supply, star mix, review scores and sustainability certification per city.
- **Availability monitoring** — `isSoldOut`, `onlyXLeftMessage` and `freeCancellationUntil` per offer.

### Proxy

**A proxy is required.** Booking's AWS WAF challenge never clears from Apify's own datacenter IPs —
a run with `proxyConfiguration: { "useApifyProxy": false }` fails with "search results never
rendered behind the challenge" after three attempts. The default Apify proxy clears it. Escalate to
`RESIDENTIAL` if the challenge starts failing there too.

The token is minted through one exit IP and bound to it, so the browser and every later request
share a single sticky session. When the challenge fails, the actor retries on a fresh exit IP up to
three times before giving up, because the challenge refuses individual IPs intermittently.

# Actor input Schema

## `search` (type: `string`):

City, region, landmark or address to search, exactly as you would type it on Booking.com.

## `startUrls` (type: `array`):

Booking.com URLs to start from: search results (`/searchresults.html?ss=…` or `?dest_id=…&dest_type=city`), SEO destination pages (`/city/nl/amsterdam.html`) or a single property page (`/hotel/nl/…​.html`). Filters carried on the URL are applied; anything you set below wins on conflict.

## `maxItems` (type: `integer`):

Hotels to return per destination or URL. Booking stops serving new properties for one search past ~1000 results; the run reports it in the status message when that ceiling is what stopped it.

## `checkIn` (type: `string`):

YYYY-MM-DD. Prices on Booking.com only exist for a given stay; leave both dates empty and the actor uses a 1-night stay 30 days out.

## `checkOut` (type: `string`):

YYYY-MM-DD. Defaults to the day after check-in.

## `flexWindow` (type: `string`):

Let Booking shift the stay up to this many days around your dates.

## `rooms` (type: `integer`):

Rooms the price should cover.

## `adults` (type: `integer`):

Adults the price should cover.

## `children` (type: `integer`):

Children the price should cover. Ages default to 8 unless you set Children ages.

## `childrenAges` (type: `array`):

One age (0-17) per child. Booking prices children by age, so setting this changes the price you get back. Not offered by the incumbent scraper.

## `propertyType` (type: `string`):

Only return properties of this type. Resolved against the property-type list Booking returns for your destination, so an unavailable type fails loudly instead of being ignored.

## `sortBy` (type: `string`):

Hotel attribute by which the results will be ordered.

## `minScore` (type: `string`):

Minimum guest review score, in "8.4" format. Booking only offers 5.0/6.0/7.0/8.0/9.0 buckets, so the value is rounded down to the nearest one.

## `starsCountFilter` (type: `string`):

Only return properties with this star rating.

## `minMaxPrice` (type: `string`):

Minimum and maximum price per night, e.g. '100-150' or '100+'. In the currency below.

## `currency` (type: `string`):

Currency prices are returned in (ISO 4217).

## `language` (type: `string`):

Language Booking.com renders names and labels in.

## `proxyConfiguration` (type: `object`):

Booking.com fronts every page with an AWS WAF challenge. The default Apify proxy is recommended; escalate to RESIDENTIAL if the challenge starts failing.

## Actor input object example

```json
{
  "search": "Amsterdam",
  "startUrls": [],
  "maxItems": 25,
  "flexWindow": "0",
  "rooms": 1,
  "adults": 2,
  "children": 0,
  "childrenAges": [],
  "propertyType": "none",
  "sortBy": "distance_from_search",
  "starsCountFilter": "any",
  "minMaxPrice": "0-999999",
  "currency": "USD",
  "language": "en-gb",
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `hotels` (type: `string`):

All hotel records. Append ?format=csv for CSV.

## `datasetUrl` (type: `string`):

The default dataset.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "search": "Amsterdam",
    "maxItems": 25,
    "sortBy": "distance_from_search",
    "starsCountFilter": "any",
    "minMaxPrice": "0-999999",
    "currency": "USD",
    "language": "en-gb",
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("diopside/booking-hotels").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "search": "Amsterdam",
    "maxItems": 25,
    "sortBy": "distance_from_search",
    "starsCountFilter": "any",
    "minMaxPrice": "0-999999",
    "currency": "USD",
    "language": "en-gb",
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("diopside/booking-hotels").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "search": "Amsterdam",
  "maxItems": 25,
  "sortBy": "distance_from_search",
  "starsCountFilter": "any",
  "minMaxPrice": "0-999999",
  "currency": "USD",
  "language": "en-gb",
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call diopside/booking-hotels --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,diopside/booking-hotels"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/jgI9KN3nxT4NfkfhA/builds/8UQBauEbbmLEI1szz/openapi.json
