# Booking Scraper (`scraper-engine/booking-scraper`) Actor

🏨 Booking Scraper pulls structured hotel data from Booking.com — prices, availability, reviews, ratings, photos, amenities & location. ⚙️ Scheduling, proxy-ready, retries. 📊 Export CSV/JSON or API. 🚀 Ideal for price monitoring, market research, lead gen & travel SEO.

- **URL**: https://apify.com/scraper-engine/booking-scraper.md
- **Developed by:** [Scraper Engine](https://apify.com/scraper-engine) (community)
- **Categories:** Travel, Lead generation, Automation
- **Stats:** 25 total users, 6 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $4.99 / 1,000 results

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

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

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

### Booking.com Scraper — Hotel Prices, Rooms, Ratings and Facilities

Booking.com Scraper extracts public Booking.com hotel detail pages and search results into structured hotel rows: name, lowest available room price, guest rating and review count, star rating, address and GPS coordinates, room list, facilities, photos and trader/host details. Every row is typed JSON with the same 36 keys on every run — no HTML parsing, no selectors. Enter a destination or paste Booking.com URLs and press Start.

⚠️ **Read this before your first run.** Thirteen of the eighteen inputs — dates, occupancy, currency, language, sorting and every filter — are used **only** to build a destination search. When you supply `startUrls`, the Actor fetches those URLs exactly as given and those inputs have no effect. Put your dates and filters in the URL query string instead.

### What is Booking.com Scraper?

Booking.com Scraper is an Apify Actor that turns a destination search or a list of Booking.com URLs into a clean accommodation dataset. It drives a headless Chromium browser through an Apify Residential proxy, clears Booking.com's AWS WAF challenge, then reads each property page's embedded Apollo GraphQL state, schema.org JSON-LD and `window.booking` variables — merging all three into one row per property.

No Booking.com account, login or cookie is required. Every field it returns is visible to any anonymous visitor on the property page.

It is built for revenue managers benchmarking competitor rates, travel analysts and OTAs building accommodation feeds, and developers piping hotel supply and pricing data into their own tools.

### What Booking.com hotel data is publicly available to scrape?

Booking.com renders the full public offer on a property page before any login: name, description, star rating, guest score, address, photos, room types, facilities, house rules and — when a date range is set — availability pricing. What sits behind a gate is the guest's own booking record, not the property data.

| Data Category | Publicly Available | Behind a Booking.com account / partner login |
| ----- | ----- | ----- |
| Property name, description, type, star rating | ✅ Public | — |
| Guest review score, review count, category sub-scores | ✅ Public | — |
| Street address, city, country, GPS coordinates | ✅ Public | — |
| Room names, sizes, occupancy and per-room prices | ✅ Public — needs a date range in the request | — |
| Property and room photo galleries | ✅ Public | — |
| Facilities, highlights, house rules, fine print | ✅ Public | — |
| Trader / host identity and registration details | ✅ Public — Booking.com publishes these under EU trader-transparency rules | — |
| Individual guest review text and reviewer names | ❌ Not returned by this Actor | — |
| Your own reservations, payments and messages | ❌ | Account |
| Extranet rates, occupancy and revenue reports | ❌ | Partner extranet |

Booking.com Scraper only returns publicly visible data — what any visitor sees on a property page. Nothing behind a login wall.

### What data can I extract with Booking.com Scraper?

Every row carries five groups of data: what the property is, what it costs, where it is, what is inside it, and who legally operates it.

| Field Name | Description |
| ----- | ----- |
| `order` | 1-based position of the property in this run's URL list |
| `url` | Canonical property URL from the page's JSON-LD, falling back to the URL fetched |
| `source_url` | The exact Booking.com URL the Actor fetched for this row |
| `startUrlOrQuery` | The `search` value if one was supplied, otherwise the property URL |
| `name` | Property name |
| `type` | Booking.com's property-type anchor; the literal string `"hotel"` when the page does not expose one |
| `description` | Long-form property description |
| `stars` | Official star rating as a number, `null` for unrated properties |
| `price` | Lowest room price found on the page — see the pricing note below |
| `currency` | Currency code the **property** quotes in, from Booking.com's own page variables; `"USD"` when absent |
| `rating` | Guest review score out of 10, as a float |
| `ratingLabel` | Booking.com's localised score word, e.g. `"Superb"` |
| `reviews` | Number of guest reviews, integer, `0` when absent |
| `breakfast` | `"Available"` when a breakfast signal is found, otherwise `null` |
| `checkIn` | The property's check-in **time** window, e.g. `"15:00 - 00:00"` |
| `checkOut` | The property's check-out **time** window, e.g. `"Until 12:00"` |
| `checkInDate` | The `checkIn` date you supplied as input, normalised to `YYYY-MM-DD` |
| `checkOutDate` | The `checkOut` date you supplied as input, normalised to `YYYY-MM-DD` |
| `location` | Object: `lat`, `lng` — GPS coordinates as floats |
| `address` | Object: `full`, `city`, `country` |
| `image` | First photo URL found, `""` when none |
| `images` | Property photo URLs, capped at 50 by the Actor |
| `roomImages` | Array of objects: `roomId`, `images` — photos grouped per room type |
| `rooms` | Array of room objects: `id`, `name`, `description`, `size`, `occupancy`, `price` |
| `facilities` | Array of objects: `name`, `id` |
| `highlights` | Facility-highlight titles as a string array |
| `finePrint` | Booking.com's fine-print paragraphs joined into one string |
| `policies` | Real house-rule phrases — check-in requirements, age limits, groups, smoking, quiet hours, parties, curfew, city regulations |
| `categoryReviews` | Array of objects: `name`, `score` — per-category guest sub-scores such as Staff, Cleanliness, Location |
| `hotelChain` | Chain or brand name, `null` for independents |
| `hotelId` | Booking.com's internal property ID, as a string |
| `licenseInfo` | First licence / registration number published for the property |
| `hostInfo` | Host display name, or the trader's first and last name when no company name is published |
| `traderInfo` | Object with the property's legal trader record — see the table below |
| `breadcrumbs` | Array of objects: `name`, `url` — Booking.com's own location hierarchy for the property |
| `timeOfScrapeISO` | UTC ISO 8601 timestamp of when the row was built |

> `source_url` is the only snake\_case key in the row — every other key is camelCase. It is kept that way so existing field mappings do not break.

#### Identity, location and rating fields

`name`, `hotelId`, `type`, `description` and `stars` identify the property. `address` gives you `full` (street line only), `city` and `country` as an uppercased ISO code, and `location` gives `lat` / `lng` as floats for geo-analysis. `breadcrumbs` carries Booking.com's own country → region → city hierarchy, which is more consistent than parsing the address string.

`rating`, `reviews`, `ratingLabel` and `categoryReviews` are the guest-sentiment block. `ratingLabel` comes from Booking.com's localised score text when the page provides it; when it does not, the Actor derives it from the score and returns an English word (`Superb` at 9+, `Very good` at 8+, `Good` at 7+, `Pleasant` at 6+) regardless of the `language` you selected. Scores under 6 with no upstream label return `null`.

#### Price, room and availability fields

`price` is the **lowest** price found across the property's room blocks — not a nightly average and not a labelled stay total. Three sources are tried in order: the minimum room-block amount from the Apollo state, then Booking.com's own `b_cheapest_price_that_fits_search_eur` page variable, then the visible price element in the DOM.

That middle fallback is denominated in euros by Booking.com regardless of your `currency` input, while the `currency` field reports the currency the *property* quotes in. If you need a guaranteed currency match, read `rooms[].price` — those come from the room blocks in the page's own currency — and treat a `price` with no matching room entry as approximate.

`rooms` gives you `name`, `description`, `size`, `occupancy` (max guests) and `price` per room type. Room-level prices and availability only appear when Booking.com has a date range to price against, which is why the input schema groups `checkIn` and `checkOut` under *"Set dates to get additional information (e.g. price)"*. Run without dates and expect `price` to be `null` on many properties and `rooms` to carry names only.

#### Facilities, policy and trader fields

`facilities`, `highlights`, `finePrint` and `policies` describe what the property offers and what it demands. `policies` is built from Booking.com's real house-rule phrases rather than being inferred from the facility list, and returns `[]` when a page publishes none. `breakfast` is `"Available"` or `null` only — it never returns a negative, so `null` means "no breakfast signal found", not "no breakfast".

`traderInfo` is the property's legal operator record, which Booking.com publishes for EU trader transparency:

| `traderInfo` key | Description |
| ----- | ----- |
| `isBusiness` | Whether Booking.com flags the operator as a trader |
| `companyName` | Registered legal company name |
| `firstName` | Trader contact first name, for individually operated properties |
| `middleName` | Trader contact middle name |
| `lastName` | Trader contact last name |
| `email` | Published trader email address |
| `phone` | Published trader phone number |
| `registrationNumber` | Company registration number |
| `tradeRegisterName` | Name of the trade register the company is entered in |
| `address` | Object: `street`, `street2`, `postalCode`, `city`, `countryCode`, `state` |

`traderInfo.address.postalCode` is the only place a postal code appears — the top-level `address.full` is the street line only. `licenseInfo` returns the **first** licence number published for the property, even when the page lists several.

#### 🤖 Add-on: Need additional accommodation data?

Two Scraper-Engine Actors pair naturally with this one. **Apartments.com Scraper** covers the long-stay and residential rental side of the same market, with rent, beds, baths and availability. **Google Maps Scraper With Reviews Sentiment Analysis** adds guest review text and sentiment for the same properties, which this Actor deliberately does not collect.

### Why not build this yourself?

Booking.com's programmatic route is the Demand API, and it is not self-serve: its published prerequisites require you to be a registered **Booking.com Managed Affiliate Partner**, with Partner Centre access "provided by your Booking.com Account Manager after signing the agreed contract", before you can generate the API key and `X-Affiliate-Id` a request needs ([Booking.com Demand API prerequisites](https://developers.booking.com/demand/docs/getting-started/prerequisites), checked 25 July 2026). Without that partnership, scraping the public pages is the alternative — and the cost sits in four places.

**Anti-bot.** Booking.com is protected by AWS WAF. A browser-impersonating HTTP client gets 302-redirected, and a single Playwright `goto` does not clear the JavaScript challenge — you get a challenge shell of a few kilobytes instead of a page. Clearing it means a real Chromium, waiting for the `aws-waf-token` cookie to be issued, reloading until the genuine page renders, and — critically — keeping **one** browser context alive for the whole run so the solved token is reused. The obvious block check does not work either: a solved page still inlines the WAF challenge script, so detection has to be based on page size and page markers rather than on the WAF string.

**Egress.** Datacenter IPs do not survive this. Residential proxying is a running cost before a single row is parsed.

**Parameter surface.** A Booking.com search URL is not one flat query string. Dates, occupancy and currency are top-level parameters; star rating, property type and minimum review score are all packed into a single semicolon-joined `nflt` value using numeric property-type IDs; price range splits across `price_range_min` / `price_range_max`; language changes the **domain path** of the search endpoint, not just a parameter. Pagination is a 25-step `offset`.

**Extraction.** Property pages do not put their data in the visible HTML. It lives in an Apollo GraphQL store full of `__ref` pointers you have to resolve by hand, alongside schema.org JSON-LD and inline `window.booking` variables — with different fields reliable in each. This Actor merges all four layers and falls back through them per field, so a moved key degrades one field rather than failing the row.

### How to use Booking.com Scraper

Booking.com Scraper runs on Apify. Start it from the Apify Console or call it through the Apify API.

1. Open Booking.com Scraper on Apify and click **Try for free**
2. Type a city, region or landmark into **Destination** (`search`) — or paste Booking.com URLs into **Start URLs** instead
3. Set **Check-in date** and **Check-out date**. No input is marked required in the schema, but without dates Booking.com has nothing to price against and `price` and `rooms[].price` will often be `null`
4. Set **Max results** (`maxItems`) and adjust occupancy — **Number of rooms**, **adults**, **children**
5. Optionally narrow the search with **Property type**, **Stars count**, **Minimum rating**, **Price range** and **Order results by**
6. Leave **Proxy Configuration** on Apify Residential — Booking.com blocks direct traffic
7. Click **Start**, then export the dataset as JSON, CSV, Excel, XML or HTML

A run with an empty `search` **and** an empty `startUrls` logs "Nothing to do" and exits without pushing any rows.

To build a start URL, run the search you want on booking.com in your browser and copy the address bar — filters, dates and occupancy travel with the link. That is the only way to apply them in `startUrls` mode.

#### How to scale to bulk hotel extraction

`startUrls` is a list and accepts both property detail pages and search-results pages. A detail page becomes one row; a search-results page is paginated in steps of 25 until it has yielded enough properties. Plain strings and `{ "url": "..." }` objects both work, and an undocumented `urls` key is accepted as an alias. Any entry that does not contain `booking.com` is silently dropped.

Two limits to plan around. First, `search` accepts a single destination string, not a list — for many cities, either run the Actor once per city or paste one search-results URL per city into `startUrls`. Second, `maxItems` is applied as a **total across the whole run**, despite its title reading "per destination or URL": the combined list of property URLs is truncated to `maxItems` before scraping begins. Ten detail URLs with `maxItems: 5` scrapes five properties, not ten. Set `maxItems` to the total row count you want.

### What can you do with Booking.com hotel data?

- 💰 **A revenue manager** benchmarking a property re-runs the same destination and date range weekly, then diffs `price` and `rooms[].price` per `hotelId` to see where competitors moved before it shows up in their own occupancy.
- 🗺️ **A travel analyst** mapping supply in a city pulls `location.lat`, `location.lng`, `stars` and `type` across a search and plots pricing tiers by district using `breadcrumbs`.
- 🏨 **An OTA product team** building a comparison page ingests `name`, `images`, `facilities`, `rating` and `reviews` to populate property cards without assembling them by hand.
- 📋 **A compliance researcher** auditing short-let regulation in a market reads `licenseInfo`, `traderInfo.registrationNumber` and `traderInfo.isBusiness` to separate registered operators from unregistered listings.
- ⭐ **A market researcher** studying guest satisfaction ranks properties on `rating`, `reviews` and `categoryReviews` to find where cleanliness or staff scores diverge from the headline score.
- 🤖 **An AI engineer** building a travel assistant indexes `description`, `facilities`, `policies` and `finePrint` into a vector store, so an agent can answer "which 4-star hotels near this district allow late check-in and include breakfast" against live property data instead of a stale snapshot.

Every one of these is callable from an agent framework over the Apify API, since the Actor is a standard HTTP-triggered run.

### How does Booking.com Scraper handle rate limits and blocking?

Booking.com is protected by AWS WAF, and this Actor is built specifically around clearing it.

Every page is loaded in a headless Chromium browser via Playwright, routed through an Apify Residential proxy, with a desktop Chrome 131 user agent, a 1366×900 viewport, `en-GB` locale and automation-control blink features disabled. One browser context is created and reused for the entire run, so the `aws-waf-token` cookie earned by the first successful load is carried into every subsequent page load rather than being re-solved each time.

A page is accepted as real only when the HTML exceeds 60,000 characters **and** contains either the Apollo store marker or a property-card marker — the WAF challenge string itself is not used as the block signal, because Booking.com inlines it on solved pages too. If a load does not pass that check, the Actor waits up to 15 seconds for the WAF token cookie to appear, pauses, and reloads, up to five times. If all five reloads fail, it tears the browser context down and rebuilds it on a fresh proxy IP, up to three times per URL. Navigation timeout is 90 seconds per attempt. Pages are fetched one at a time, with a one-second pause between search-result pages.

There is no third-party CAPTCHA-solving service involved, and none is required from you. When a property page cannot be recovered after all three context rotations — or when no property name can be extracted from the HTML that did load — the row is skipped with a log line and the run continues. No partial or placeholder row is written, so a blocked page lowers your row count rather than corrupting the dataset.

### ⬇️ Input

No input is marked required in the schema. In practice you need either `search` or `startUrls`. All eighteen parameters below match the Actor's input schema exactly.

| Parameter | Required | Type | Description | Example Value |
| ----- | ----- | ----- | ----- | ----- |
| `search` | No | string | City, region or landmark to search for accommodation. Ignored when Start URLs are provided. Single destination only — not a list. Default `""`. | `"New York"` |
| `startUrls` | No | array | One or more Booking.com URLs: property detail pages or search-results pages. Plain strings and `{ "url": "..." }` objects both accepted. Non-Booking.com entries are dropped. Default `[]`. | `["https://www.booking.com/hotel/gb/the-savoy.html"]` |
| `maxItems` | No | integer | Maximum number of hotels to scrape. Applied as a total across the whole run. Minimum 1, maximum 20000. Default `10`. | `50` |
| `scrapeAdditionalHotelData` | No | boolean | Extract the full add-on blocks — rooms, facilities, highlights, images, roomImages, breadcrumbs, categoryReviews, finePrint, policies, hotelChain, licenseInfo, hostInfo, traderInfo. Default `true`. | `true` |
| `propertyType` | No | string | Filter by accommodation type. 15 options: `none` (Any), `hotels`, `apartments`, `hostels`, `guest houses`, `homestays`, `bed and breakfasts`, `holiday homes`, `boats`, `villas`, `motels`, `resorts`, `holiday parks`, `campsites`, `luxury tents`. Default `"none"`. | `"apartments"` |
| `sortBy` | No | string | How to sort search results: `distance_from_search` (Distance from city centre), `price` (Price, lowest first), `review_score_and_price` (Review score and price), `review_score` (Review score), `class` (Star rating). Default `"distance_from_search"`. | `"review_score"` |
| `minimumRating` | No | string | Minimum guest rating, e.g. `7`, `8`, `9`. Truncated to a whole number before use. Empty means no filter. Default `""`. | `"8"` |
| `starsCountFilter` | No | string | Filter by star rating: `any`, `1`, `2`, `3`, `4`, `5`. Default `"any"`. | `"4"` |
| `currency` | No | string | Currency for displayed prices. 11 options: `USD`, `EUR`, `GBP`, `CAD`, `AUD`, `CHF`, `JPY`, `CNY`, `INR`, `BRL`, `MXN`. Default `"USD"`. | `"GBP"` |
| `language` | No | string | Interface language for the search; also selects the Booking.com search domain path. 12 options: `en-gb`, `en-us`, `de`, `fr`, `es`, `it`, `pt-br`, `nl`, `pl`, `ru`, `ja`, `zh`. Default `"en-gb"`. | `"en-gb"` |
| `checkIn` | No | string | Absolute date `YYYY-MM-DD` or relative, e.g. `2 weeks`. Empty means a flexible search with no dates. Default `""`. | `"2026-09-14"` |
| `checkOut` | No | string | Absolute date `YYYY-MM-DD` or relative, e.g. `1 week`. Empty means a flexible search with no dates. Default `""`. | `"2026-09-17"` |
| `flexWindow` | No | string | Allow the stay dates to shift by this many days: `0` (Exact dates only), `1`, `2`, `3`, `7`. Default `"0"`. | `"0"` |
| `rooms` | No | integer | Number of rooms for the stay. Minimum 1, maximum 9. Default `1`. | `1` |
| `adults` | No | integer | Number of adult guests. Minimum 1, maximum 30. Default `2`. | `2` |
| `children` | No | integer | Number of children. Minimum 0, maximum 30. Default `0`. | `0` |
| `minMaxPrice` | No | string | Min-max price filter, e.g. `50-200`, or `100+` for a minimum only. The default value sends no filter at all. Default `"0-999999"`. | `"80-300"` |
| `proxyConfiguration` | No | object | Apify Proxy settings. Booking.com blocks direct traffic, so Residential is prefilled. Default `{"useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"]}`. | `{"useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"]}` |

**Inputs that do not behave the way their names suggest.** These are worth knowing before you debug a run:

- **`startUrls` overrides thirteen inputs, not just `search`.** `checkIn`, `checkOut`, `flexWindow`, `rooms`, `adults`, `children`, `currency`, `language`, `sortBy`, `propertyType`, `starsCountFilter`, `minimumRating` and `minMaxPrice` are only ever used to construct a destination search URL. In `startUrls` mode that construction never happens, so those values go unused — put them in the URL query string. The one exception is cosmetic: `checkInDate` and `checkOutDate` in the output are still filled from your `checkIn` / `checkOut` inputs, so in `startUrls` mode they report what you typed, not what the page was priced for.
- ⚠️ **Relative dates are both measured from today, not from each other.** `checkIn: "2 weeks"` with `checkOut: "1 week"` produces a check-out date *before* the check-in date. Relative units are approximated as 30-day months and 365-day years. A value matching neither the `YYYY-MM-DD` form nor the `N days/weeks/months/years` form is silently treated as empty rather than raising an error.
- **`maxItems` is a run total.** Its title says "per destination or URL", but the assembled property-URL list is truncated to `maxItems` before scraping begins.
- **`minimumRating` is truncated to an integer.** `"8.5"` becomes `8`. Values below 1 and non-numeric values are ignored.
- **`minMaxPrice` at its default sends nothing.** `"0-999999"` is treated as "no filter". A malformed range is ignored silently.
- **`proxyConfiguration` only honours groups and country.** The Actor always requests an Apify proxy for the browser; setting `useApifyProxy: false` does not disable it, and there is no field for supplying an external proxy URL. `apifyProxyGroups` and `apifyProxyCountry` are respected, and `RESIDENTIAL` is used when no group is given.
- **`scrapeAdditionalHotelData: false` does not lower cost.** It empties the add-on blocks in the output — the seven arrays become `[]`, and `finePrint`, `hotelChain`, `licenseInfo`, `hostInfo` and `traderInfo` become `null` — but the keys stay present and charging is per pushed row either way.
- **`search` still shows up in the output in URL mode.** `startUrlOrQuery` prefers `search` when it is non-empty, so if you fill in both fields, that column shows the destination string that was never actually searched.

#### Example input

```json
{
  "search": "Amsterdam",
  "startUrls": [],
  "maxItems": 25,
  "scrapeAdditionalHotelData": true,
  "propertyType": "hotels",
  "sortBy": "review_score",
  "minimumRating": "8",
  "starsCountFilter": "4",
  "currency": "EUR",
  "language": "en-gb",
  "checkIn": "2026-09-14",
  "checkOut": "2026-09-17",
  "flexWindow": "0",
  "rooms": 1,
  "adults": 2,
  "children": 0,
  "minMaxPrice": "80-300",
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": ["RESIDENTIAL"]
  }
}
```

### ⬆️ Output

Every property is one dataset row with the same 36 keys in the same order, run after run. Rows are pushed live as each property page finishes, so the dataset fills while the run is still going. Nested values arrive as plain JSON objects and arrays, and absent values come back as `null`, `""` or `[]` rather than as a missing key — downstream code never needs a presence check. Export as JSON, CSV, Excel, XML or HTML, or read the dataset through the Apify API.

Only successfully extracted properties are written. There are no error or accounting rows in the dataset, and therefore no `errorReason` field and no filter expression you need to exclude anything — every row is a real property. Failures appear in the run log only (`[HOTEL] Could not load ...` for an uncleared WAF challenge, `[HOTEL] No hotel name extracted ...` for a page that loaded but yielded nothing). Compare your returned item count against `maxItems` to detect them.

#### Example output

```json
{
  "order": 3,
  "url": "https://www.booking.com/hotel/nl/pulitzeramsterdam.html",
  "startUrlOrQuery": "Amsterdam",
  "name": "Pulitzer Amsterdam",
  "type": "hotel",
  "description": "Set across 25 canal houses in Amsterdam's Nine Streets district, Pulitzer Amsterdam offers individually designed rooms, a courtyard garden and two on-site restaurants.",
  "stars": 5,
  "price": 428,
  "currency": "EUR",
  "rating": 8.9,
  "ratingLabel": "Fabulous",
  "reviews": 4127,
  "breakfast": "Available",
  "checkIn": "15:00 - 00:00",
  "checkOut": "Until 12:00",
  "checkInDate": "2026-09-14",
  "checkOutDate": "2026-09-17",
  "location": { "lat": 52.373611, "lng": 4.885 },
  "address": {
    "full": "Prinsengracht 323",
    "country": "NL",
    "city": "Amsterdam"
  },
  "image": "https://cf.bstatic.com/xdata/images/hotel/max1024x768/318472901.jpg",
  "rooms": [
    {
      "id": "1204580103",
      "name": "Collectors Room",
      "description": "A canal-house room with original wooden beams and a curated art selection.",
      "size": 22,
      "occupancy": 2,
      "price": 428
    },
    {
      "id": "1204580107",
      "name": "Deluxe Room Canal View",
      "description": "Larger room overlooking the Prinsengracht canal.",
      "size": 30,
      "occupancy": 3,
      "price": 596
    }
  ],
  "highlights": ["Free WiFi", "Bar", "Room service", "Non-smoking rooms"],
  "finePrint": "Please note that the property has no on-site parking. Guests arriving by car can use the nearby public garage at an additional charge.",
  "policies": [
    "Guests are required to show a photo identification and credit card upon check-in.",
    "Smoking is not allowed in any of the rooms.",
    "Parties/events are not allowed."
  ],
  "images": [
    "https://cf.bstatic.com/xdata/images/hotel/max1024x768/318472901.jpg",
    "https://cf.bstatic.com/xdata/images/hotel/max1024x768/318472933.jpg",
    "https://cf.bstatic.com/xdata/images/hotel/max1024x768/318473012.jpg"
  ],
  "roomImages": [
    {
      "roomId": "1204580103",
      "images": [
        "https://cf.bstatic.com/xdata/images/hotel/max1024x768/318475221.jpg",
        "https://cf.bstatic.com/xdata/images/hotel/max1024x768/318475238.jpg"
      ]
    }
  ],
  "categoryReviews": [
    { "name": "Staff", "score": 9.4 },
    { "name": "Facilities", "score": 8.8 },
    { "name": "Cleanliness", "score": 9.1 },
    { "name": "Comfort", "score": 9.0 },
    { "name": "Location", "score": 9.6 },
    { "name": "Value", "score": 8.2 }
  ],
  "hotelChain": null,
  "licenseInfo": "0363 1B21 8A5C 9F00 0001",
  "hostInfo": null,
  "traderInfo": {
    "isBusiness": true,
    "email": "reservations@pulitzeramsterdam.com",
    "phone": "+31205235235",
    "companyName": "Pulitzer Amsterdam B.V.",
    "firstName": null,
    "middleName": null,
    "lastName": null,
    "registrationNumber": "34118831",
    "tradeRegisterName": "Kamer van Koophandel",
    "address": {
      "street": "Prinsengracht 323",
      "street2": null,
      "postalCode": "1016 GZ",
      "city": "Amsterdam",
      "countryCode": "NL",
      "state": null
    }
  },
  "breadcrumbs": [
    { "name": "Netherlands", "url": "https://www.booking.com/country/nl.html" },
    { "name": "North Holland", "url": "https://www.booking.com/region/nl/north-holland.html" },
    { "name": "Amsterdam", "url": "https://www.booking.com/city/nl/amsterdam.html" }
  ],
  "hotelId": "42391",
  "facilities": [
    { "name": "Free WiFi", "id": 107 },
    { "name": "Bar", "id": 11 },
    { "name": "Fitness Centre", "id": 11 },
    { "name": "Airport Shuttle", "id": 17 }
  ],
  "timeOfScrapeISO": "2026-07-25T09:41:18.204731+00:00",
  "source_url": "https://www.booking.com/hotel/nl/pulitzeramsterdam.html"
}
```

### How does it work?

Booking.com puts its property data in an embedded GraphQL store rather than in the visible HTML — and it puts an AWS WAF challenge in front of the page.

Booking.com Scraper opens a headless Chromium browser through an Apify Residential proxy and keeps a single browser context alive for the whole run. The first page load triggers the WAF challenge; the Actor waits for the `aws-waf-token` cookie, reloads until a genuine page renders, then reuses that solved token for every later page. In destination mode it builds a Booking.com search URL from your filters, walks the result pages in steps of 25 collecting property links, then visits each property page in turn.

Each page is parsed once into four layers — the Apollo GraphQL store, schema.org JSON-LD, inline `window.booking` variables, and `data-testid` DOM selectors — and every field falls back through them in that order. Because most fields come from structured payloads rather than rendered markup, a Booking.com visual redesign generally does not affect the output, and your key names and types stay the same. Only publicly visible property data is collected; no account, cookie or login session is used.

### Integrations

Booking.com Scraper is an Apify Actor, so it works with anything that can call the Apify API or consume a dataset.

#### Calling Booking.com Scraper from Python

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")

run = client.actor("<YOUR_USERNAME>/booking-scraper").call(run_input={
    "search": "Amsterdam",
    "checkIn": "2026-09-14",
    "checkOut": "2026-09-17",
    "adults": 2,
    "currency": "EUR",
    "maxItems": 25,
    "proxyConfiguration": {"useApifyProxy": True, "apifyProxyGroups": ["RESIDENTIAL"]},
})

for hotel in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(hotel["name"], hotel["price"], hotel["currency"], hotel["rating"], hotel["reviews"])
```

Works in Go, Ruby, Node.js, cURL — any language that can make an HTTP request.

#### Scheduling and webhooks

Use Apify **Schedules** to re-run the same destination and date range daily or weekly — that is how you build a rate-history series, since each run writes its own dataset and every row is stamped with `timeOfScrapeISO`. Attach an Apify **webhook** on run success to push the finished dataset into your own endpoint, or send it to Google Sheets, Slack or a database through the Actor's integrations tab.

#### No-code tools (n8n, Make, Zapier)

In n8n, use the Apify node — or an HTTP Request node pointed at the Apify run endpoint with your token — and pass the same JSON input shown above; a Split Out node on `rooms` then gives you one item per room type. In Make, use the Apify module's run-and-wait action and map the dataset output into a Google Sheets or Airtable step. Both handle scheduling, so a weekly competitor-rate sweep needs no code.

### Is it legal to scrape Booking.com hotel listings?

Scraping publicly listed accommodation data is broadly treated as permissible where the data is accessible without authentication, and Booking.com Scraper collects only what an anonymous visitor already sees on a property page. No login is used and no access control is bypassed.

The bulk of the output is commercial business and product data: property names, room types, prices, availability, star ratings, aggregate guest scores, facilities and house rules. These are business records, not personal data, so the personal-data regimes that govern profile scraping do not attach to this output the way they would to a scraper that collects individual people. This Actor does not return guest review text or reviewer names.

**Two field groups are the exception and should be handled as personal data.** For individually operated properties, `traderInfo.firstName`, `traderInfo.middleName`, `traderInfo.lastName`, `traderInfo.email`, `traderInfo.phone` and `hostInfo` can identify a natural person — Booking.com publishes these under EU trader-transparency obligations, but that does not make them exempt for you. If you store or process those six fields at scale, GDPR and equivalent regimes apply and you need a lawful basis. Set `scrapeAdditionalHotelData` to `false` to drop `traderInfo` and `hostInfo` entirely if you do not need them.

What otherwise applies is contractual and database-related: Booking.com's terms of service, and database or unfair-competition rules covering systematic reuse of a compiled listings dataset. Consult legal counsel if your use case involves bulk storage of personal data, or before republishing extracted listings as a competing inventory.

### ❓ Frequently asked questions

#### What Booking.com hotel fields does Booking.com Scraper return?

The five most used are `name`, `price`, `rating`, `address` and `rooms`. Thirty-six keys are returned per row, including nested objects for `location`, `address` and `traderInfo` — see the data fields table above for all of them.

#### Does Booking.com Scraper require a Booking.com account or login?

No. No Booking.com account, no cookie, no session. The Actor loads public property pages anonymously. The only credential you need is your Apify token, and the only paid resource is the Apify Residential proxy the Actor uses by default.

#### How many hotels can I extract in one run?

`maxItems` accepts 1 to 20,000, and it is a **total across the run** rather than a per-URL figure — the assembled property-URL list is truncated to `maxItems` before scraping starts. Search results are paginated 25 at a time until the target is reached or a page returns nothing new. The Actor's configured pay-per-event prices are $0.005 for the actor start and $0.01 per hotel result pushed to the dataset.

#### Do I need check-in and check-out dates to get prices?

In practice, yes. Room-level availability and pricing only render when Booking.com has a date range to price against, which is why the schema groups these two inputs under "Set dates to get additional information (e.g. price)". Without dates, expect `price` to be `null` on many properties and `rooms` entries to carry names without prices. If you use `startUrls`, the dates must be in the URL query string — the `checkIn` / `checkOut` inputs do not modify a supplied URL.

#### Why does `price` not match the currency I selected?

Because `price` and `currency` come from different places. `currency` reports the currency the *property* quotes in, read from Booking.com's own page variables. `price` is the lowest room-block amount when one is available; when it is not, the Actor falls back to Booking.com's `b_cheapest_price_that_fits_search_eur` page variable, which is denominated in euros regardless of your `currency` input. For a guaranteed currency match, read `rooms[].price`.

#### What happens if a property page is blocked or removed?

The row is skipped. When the AWS WAF challenge cannot be cleared after five reloads across three fresh browser contexts, or when the page loads but no property name can be extracted, the Actor logs the failure and moves to the next URL. Nothing is pushed for that property — there are no error rows, no `errorReason` field and no placeholder values in the dataset. Detect it by comparing your returned item count against `maxItems` and checking the run log.

#### Can I scrape multiple Booking.com hotels or destinations at once?

Yes for hotels, with a caveat for destinations. `startUrls` is a list and accepts any mix of property detail pages and search-results pages. `search`, however, takes a single destination string — for several cities, either run the Actor once per city or paste one search-results URL per city into `startUrls`. Remember that `maxItems` caps the combined total.

#### Does Booking.com Scraper work with Claude, ChatGPT and other AI agent tools?

Yes. It is callable as a standard HTTP endpoint through the Apify API, so any agent framework that can make a request — LangChain, CrewAI, n8n, or a hand-written tool definition — can invoke it and receive typed JSON with no parsing step.

#### How does Booking.com Scraper compare to other Booking.com scrapers?

Checked on the Apify Store on 25 July 2026: `automation-lab/booking-scraper` documents 21 flat search-result fields and states it uses Playwright with residential proxies against AWS WAF. `santamaria-automations/booking-com-scraper` documents 34+ search-result fields and describes an HTTP-only architecture calling Booking.com's internal GraphQL API with no browser. `plowdata/booking-com-review-scraper` covers a different entity — individual guest reviews and per-hotel score breakdowns — and its listing notes that Booking.com blocks shared Apify proxies. Room-level detail, house rules and trader records are not documented on any of the three listings. Booking.com Scraper's difference is that it opens each **property page** rather than reading only the search-results card: that is where `rooms` with per-room prices and occupancy, `facilities`, `policies`, `finePrint`, `roomImages`, `categoryReviews`, `licenseInfo` and the full `traderInfo` legal record come from. The trade-off is honest — one browser page load per property is slower than parsing a results grid.

#### Does Booking.com Scraper return data in a format LLMs can use directly?

Yes. Every row is typed, normalized JSON with the same field names on every run. No HTML parsing, no selectors. Pass a row straight into an LLM context window, index it into a vector store, or hand it to an agent tool.

#### What happens when Booking.com changes its layout or anti-bot system?

The scraper is maintained, and because most fields are read from Booking.com's embedded Apollo store and JSON-LD rather than from rendered markup, a visual redesign generally does not affect them. The layered fallback — Apollo, then JSON-LD, then `window.booking` variables, then DOM selectors — means a structural change usually degrades a single field instead of failing the row. Your field names and types do not change on your end.

#### Can I use Booking.com Scraper without managing proxies or browser infrastructure?

Yes. The Actor launches and manages its own Playwright Chromium instance, requests an Apify proxy for it, clears the AWS WAF challenge, keeps the solved token in a reused browser context, and rotates to a fresh context and IP when a page fails. You never install a browser or create a proxy account. You can pick proxy groups and a country through `proxyConfiguration`; the browser always runs through an Apify proxy.

#### Which Booking.com fields work best for AI training data and RAG indexing?

For RAG indexing: `description`, `policies`, `finePrint`, `facilities` and `highlights` carry the most descriptive text per property and chunk cleanly by `hotelId`. For training data: `price`, `rating`, `reviews`, `stars` and `categoryReviews[].score` are the most structurally consistent fields across records, and `location.lat` / `location.lng` give you clean numeric geo features. All values return as typed primitives, arrays or plain nested objects, so no normalization pass is needed before indexing.

### 🔗 Related scrapers

| Scraper Name | What it extracts |
| ----- | ----- |
| Apartments.com Scraper | Rental listings with rent, beds, baths, amenities and availability |
| Google Maps Scraper With Reviews Sentiment Analysis | Place records plus review text and sentiment for the same properties |
| Realtor.com Agents Scraper | Real-estate agent records for the residential side of a market |
| Google News Scraper By Publisher Site Filter | Destination and travel-market news filtered by publisher |
| Quora Search Scraper | Question-intent results for destination and travel content research |

### 💬 Your feedback

Found a bug, or need a field that is in Booking.com's page payload but not in the output? Open an issue on the Actor's Issues tab and it will be looked at. Reports that include the exact input JSON and the Booking.com URL you ran are the fastest to reproduce and fix.

# Actor input Schema

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

City, region or landmark to search for accommodation (e.g. New York, Paris). Ignored when Start URLs are provided.

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

Add one or more Booking.com URLs: hotel detail pages or search results pages.

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

Maximum number of hotels to scrape per destination or per start URL.

## `scrapeAdditionalHotelData` (type: `boolean`):

Extract full hotel data including rooms, facilities, trader info and images.

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

Filter by type of accommodation.

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

How to sort search results.

## `minimumRating` (type: `string`):

Minimum guest rating (e.g. 7, 8, 9). Leave empty for no filter.

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

Filter by star rating.

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

Currency for displayed prices.

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

Interface language for the search.

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

Absolute date (YYYY-MM-DD) or relative (e.g. 2 weeks). Leave empty for flexible search.

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

Absolute date (YYYY-MM-DD) or relative (e.g. 1 week). Leave empty for flexible search.

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

Allow dates to shift by this many days. Use 0 for exact dates only.

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

Number of rooms for the stay.

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

Number of adult guests.

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

Number of children.

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

Min-max price filter, e.g. 50-200 or 100+ for minimum only.

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

Booking.com blocks direct traffic, so Residential proxy is enabled by default. You can override here.

## Actor input object example

```json
{
  "search": "New York",
  "startUrls": [],
  "maxItems": 10,
  "scrapeAdditionalHotelData": true,
  "propertyType": "none",
  "sortBy": "distance_from_search",
  "minimumRating": "",
  "starsCountFilter": "any",
  "currency": "USD",
  "language": "en-gb",
  "checkIn": "",
  "checkOut": "",
  "flexWindow": "0",
  "rooms": 1,
  "adults": 2,
  "children": 0,
  "minMaxPrice": "0-999999",
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `results` (type: `string`):

All scraped items in the Actor's 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": "New York",
    "startUrls": [],
    "maxItems": 10,
    "scrapeAdditionalHotelData": true,
    "propertyType": "none",
    "sortBy": "distance_from_search",
    "minimumRating": "",
    "starsCountFilter": "any",
    "currency": "USD",
    "language": "en-gb",
    "checkIn": "",
    "checkOut": "",
    "flexWindow": "0",
    "rooms": 1,
    "adults": 2,
    "children": 0,
    "minMaxPrice": "0-999999",
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("scraper-engine/booking-scraper").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": "New York",
    "startUrls": [],
    "maxItems": 10,
    "scrapeAdditionalHotelData": True,
    "propertyType": "none",
    "sortBy": "distance_from_search",
    "minimumRating": "",
    "starsCountFilter": "any",
    "currency": "USD",
    "language": "en-gb",
    "checkIn": "",
    "checkOut": "",
    "flexWindow": "0",
    "rooms": 1,
    "adults": 2,
    "children": 0,
    "minMaxPrice": "0-999999",
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("scraper-engine/booking-scraper").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": "New York",
  "startUrls": [],
  "maxItems": 10,
  "scrapeAdditionalHotelData": true,
  "propertyType": "none",
  "sortBy": "distance_from_search",
  "minimumRating": "",
  "starsCountFilter": "any",
  "currency": "USD",
  "language": "en-gb",
  "checkIn": "",
  "checkOut": "",
  "flexWindow": "0",
  "rooms": 1,
  "adults": 2,
  "children": 0,
  "minMaxPrice": "0-999999",
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call scraper-engine/booking-scraper --silent --output-dataset

```

## MCP server setup

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

```

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/cpjbE2waMQ4FF23oE/builds/rKYoBCDbG4zxRVzZo/openapi.json
