# Booking.Com Hotel Scraper (`rexreus/booking-com-hotel-scraper`) Actor

Turn Booking.com searches and hotel pages into clean, structured data with price history and drop/bargain flags — no browser required.

- **URL**: https://apify.com/rexreus/booking-com-hotel-scraper.md
- **Developed by:** [REXREUS D.O](https://apify.com/rexreus) (community)
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 results

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

<p align="center">
  <h1 align="center">Booking.com Hotel Scraper</h1>
  <img src="https://res.cloudinary.com/ewnqk3k9/image/upload/f_auto,q_auto/v1/apify-actors/banners/booking-com-hotel-scraper-banner?_a=BAMAROIM0" alt="Clean, structured hotel data from Booking.com — HTTP-first, no browser required" align="center">
  <p align="center">
    <strong>Turn Booking.com searches and hotel pages into clean, structured data with price history and drop/bargain flags — no browser required.</strong>
  </p>
  <p align="center">
    <em>HTTP-first · runs on Apify · datacenter proxy by default, residential opt-in · local runs without proxy may be PARTIAL</em>
  </p>
  <p align="center">
    <a href="#-quick-start"><img src="https://img.shields.io/badge/Quick_Start-Apify_Console-green?style=flat-square" alt="Quick Start"></a>
    <a href="https://apify.com"><img src="https://img.shields.io/badge/Runs_on-Apify-ff443e?style=flat-square" alt="Apify"></a>
    <a href="https://nodejs.org"><img src="https://img.shields.io/badge/Node.js-≥20-339933?style=flat-square&logo=node.js&logoColor=white" alt="Node.js"></a>
  </p>
</p>

### Why this Actor?

| Feature | What you get |
|---|---|
| HTTP-first scraping | No browser, no heavy infrastructure. Bookings public pages are fetched directly, so runs are fast and cheap. |
| Search + direct hotel URLs | Paste a Booking.com search results URL, a list of hotel URLs, or both — scrape the properties you actually care about. |
| Date-scoped prices | Prices are quoted for your specific check-in/check-out dates via `searchresults.html`, or labeled `city_indicative` when only indicative nightly rates are available. |
| Price history (cross-run) | The Actor stores a 90-day price history per hotel and flags drops with `priceDropped` + `priceChangePct` compared with the previous snapshot. |
| Bargain flag | Optionally flags properties priced at or below the destination cohort median — spot the best value in a market at a glance. |
| 20 currencies | USD, EUR, IDR, SGD, JPY, GBP, AUD, THB, MYR, CNY, INR, KRW, VND, PHP, HKD, TWD, BRL, MXN, CAD, CHF. |
| Detail enrichment | Fetches each hotel page for JSON-LD details — address, amenities, geo, rating — as an opt-in per-result add-on. |
| Clean, typed dataset | One buyer-friendly row per hotel with `null` for absent data and a `status` enum that classifies every row (SUCCESS / PARTIAL / NOT_FOUND / BLOCKED / ERROR). |
| Export anywhere | Results export natively to JSON, CSV, or Excel from the Apify Console — schedule reruns, or pipe to a webhook. |
| Affordable | Search + history + flags at $0.002/result; detail enrichment is a separate line item you can leave off entirely. |

**Note on proxy:** the Actor defaults to Apify **Residential** proxy — Booking.com's AWS WAF blocks datacenter/AUTO traffic with 202 stubs, so Residential is required for reliable results. A **local run without proxy may return PARTIAL** rows.

### Quick start

1. Open the Actor in the Apify Console and click **Start**.
2. Easiest input — just set **Destination** (e.g. `Bandung`) plus dates. Or paste a full Booking.com search results URL (or hotel URLs) if you prefer.
3. Optionally customize currency, language, and flags, then click **Start**.
4. When the run finishes, open the **Dataset** tab and download as JSON, CSV, or Excel.

**Simplest input — just a destination (no URL needed):**

```json
{
  "destination": "Bandung",
  "checkin": "2026-11-01",
  "checkout": "2026-11-02",
  "selectedCurrency": "IDR",
  "maxResults": 100
}
```

**Advanced — full search URL (any variant accepted, e.g. `searchresults.en-gb.html`):**

```json
{
  "searchUrl": "https://www.booking.com/searchresults.html?ss=Bali&checkin=2026-11-01&checkout=2026-11-02&group_adults=2&no_rooms=1&group_children=0&selected_currency=USD&lang=en-us",
  "maxResults": 25,
  "enableDetail": true,
  "bargain": true
}
```

### How it works

```
Public search URL or hotel URLs → public Booking.com pages → clean dataset rows
```

1. You paste a `searchresults.html` search URL (or direct `/hotel/{cc}/{slug}.html` URLs, or both).
2. The Actor fetches the public pages over HTTP and parses each hotel card — name, URL, price, currency, rating, reviews, stars, free-cancellation status.
3. Optional detail pass: each hotel's own page is fetched for richer JSON-LD data (address, amenities).
4. Price snapshot is saved to history; the row's `priceDropped`, `priceChangePct`, and `bargain` flags are computed against the 90-day history.
5. Each property becomes one dataset row, classified by a `status` enum.

### Input

Three ways in — pick whichever fits:

1. **`destination`** (easiest) — just a place name, the Actor builds the search URL.
2. **`searchUrl`** — a full Booking.com search URL (any `searchresults[.lang].html` variant).
3. **`hotelUrls`** — a list of hotel page URLs.

```json
{
  "destination": "Bandung",
  "hotelUrls": [],
  "maxResults": 25,
  "selectedCurrency": "IDR",
  "checkin": "2026-11-01",
  "checkout": "2026-11-02",
  "lang": "en-us",
  "enableDetail": true,
  "bargain": true,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": ["RESIDENTIAL"]
  }
}
```

| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| `destination` | string | No\* | — | City or area name, e.g. `Bali`, `Bandung`. Builds the search URL for you. Ignored when `searchUrl` is set. |
| `searchUrl` | string | No\* | — | Full Booking.com search URL. Any `searchresults[.lang].html` variant accepted. Overrides `destination` if both set. |
| `hotelUrls` | array | No | — | `https://www.booking.com/hotel/{cc}/{slug}.html` URLs to scrape directly. |
| `maxResults` | integer | No | `25` | Maximum number of results (`1..5000`). |
| `selectedCurrency` | string | No | `USD` | Currency for prices: USD, EUR, IDR, SGD, JPY, GBP, AUD, THB, MYR, CNY, INR, KRW, VND, PHP, HKD, TWD, BRL, MXN, CAD, CHF. |
| `checkin` | string | No | — | Check-in date, `YYYY-MM-DD`. Overrides the date in `searchUrl`. |
| `checkout` | string | No | — | Check-out date, `YYYY-MM-DD`. Overrides the checkout in `searchUrl`. |
| `lang` | string | No | `en-us` | Language/locale tag, e.g. `en-us`, `id-id`, `de`. |
| `enableDetail` | boolean | No | `true` | Fetch each hotel's `/hotel/` page for JSON-LD details (address, rating, amenities). Adds a $0.006/result add-on. |
| `bargain` | boolean | No | `false` | Flag properties priced at or below the destination cohort median (needs ≥ 3 priced results). |
| `proxy` | object | No | `{useApifyProxy: true, apifyProxyGroups: ["RESIDENTIAL"]}` | Proxy config. Defaults to Residential — required for Booking.com's WAF. |

At least one of `destination`, `searchUrl`, or `hotelUrls` is required. `searchUrl` wins over `destination`; dates/currency from input fields override those in the URL.

### Output

One real record from a live run (Bandung search, `priceBasis: date_scoped`):

```json
  {
    "name": "Alqueby Hotel Bandung",
    "url": "https://www.booking.com/hotel/id/alqueby-bandung.html",
    "hotelId": "hotel-id-alqueby-bandung",
    "city": "Kiaracondong, Bandung",
    "country": "Indonesia",
    "price": 281250,
    "priceDisplay": "Rp 281,250",
    "currency": "IDR",
    "pricePerNight": 281250,
    "rating": 7.9,
    "reviewCount": 9,
    "stars": null,
    "location": "Kiaracondong, Bandung",
    "distance": "3.3 miles from downtown",
    "address": "ALQUEBY HOTEL",
    "freeCancellation": true,
    "priceBasis": "date_scoped",
    "priceDropped": null,
    "priceChangePct": null,
    "bargain": true,
    "parsedScrapedAt": "2026-10-13T15:23:18.757Z",
    "httpStatus": 200,
    "status": "SUCCESS",
    "error": null,
    "amenities": [],
    "images": [
      "https://cf.bstatic.com/xdata/images/hotel/square600/916087802.jpg?k=8e98453d8d9d6d72515c4c91970200881cc910fc916a4495b5cdfadff1947a6e&o="
    ]
  }
```

Download the dataset as **JSON, CSV, or Excel** directly from the Apify Console.

| Field | Type | Description |
|---|---|---|
| `name` | string | null | Hotel name. |
| `url` | string | null | Booking.com page URL. |
| `hotelId` | string | null | Stable hotel identifier. |
| `city` | string | null | City (when available). |
| `country` | string | null | Country (when available). |
| `price` | number | null | Numeric price. |
| `priceDisplay` | string | null | Display-formatted price, e.g. `Rp 281,250`. |
| `currency` | string | null | Currency code, e.g. `IDR`. |
| `pricePerNight` | number | null | Price divided by nights. |
| `rating` | number | null | Guest rating (0–10 scale). |
| `reviewCount` | number | null | Number of reviews. |
| `stars` | number | null | Hotel star rating. |
| `location` | string | null | Location string (detail pass). |
| `distance` | string | null | Distance text, e.g. `3.3 miles from downtown`. |
| `address` | string | null | Street address (detail pass). |
| `amenities` | array | Amenity list (detail pass). |
| `images` | array | Image URLs. |
| `freeCancellation` | boolean | null | Whether free cancellation is offered. |
| `priceBasis` | string | null | `date_scoped` (your dates) or `city_indicative` (indicative nightly price). |
| `priceDropped` | boolean | null | Whether price is lower than the prior snapshot. |
| `priceChangePct` | number | null | Percent change vs prior snapshot. |
| `bargain` | boolean | null | Flagged as at/below destination median (opt-in). |
| `parsedScrapedAt` | string | null | ISO timestamp of when the row was parsed. |
| `httpStatus` | number | null | HTTP status of the page fetch. |
| `status` | string | Row classification: `SUCCESS`, `PARTIAL`, `NOT_FOUND`, `BLOCKED`, `ERROR`. |
| `error` | string | null | Error detail when `status` is not `SUCCESS`. |

Fields are `null` when absent; `amenities` and `images` are always present (may be empty arrays).

### Cost, limits, proxy

- **Memory:** 512 MB (fixed).
- **Pricing:** pay-per-result. `$0.002` per hotel result (search + history + flags). `$0.006` per hotel detail enrichment when `enableDetail` is on.
- **Proxy:** defaults to Apify **Residential** proxy (required — Booking.com's WAF returns 202 challenge stubs to datacenter/AUTO traffic). Residential transfer is billed by Apify at plan rates (see `maxMonthlyResidentialProxyGbytes` on your plan). Local runs without a proxy may return `PARTIAL` rows.
- **Concurrency / limits:** `maxResults` up to `5000`; `bargain` flag needs ≥ 3 priced results in the destination cohort. Price history is kept for 90 days / last 30 runs.
- **Expected runtime:** a small run (25 hotels, no detail) takes roughly a minute; a large run (e.g. 1,000+ hotels with detail enrichment over residential proxy) takes significantly longer and costs more via the detail add-on. Plan small runs first to gauge timing for your workload.

### Recipes

#### Daily market scan with bargain flags

Track value properties in a destination and watch for drops:

```json
{
  "destination": "Bali",
  "maxResults": 50,
  "selectedCurrency": "USD",
  "checkin": "2026-11-01",
  "checkout": "2026-11-02",
  "enableDetail": false,
  "bargain": true
}
```

#### Direct hotel list, no search

Scrape exactly the properties you care about. Get URLs from a prior search run's `url` field — don't guess slugs:

```json
{
  "hotelUrls": [
    "https://www.booking.com/hotel/id/harris-tuban-bali.html",
    "https://www.booking.com/hotel/id/ayana-resort-and-spa-bali.html"
  ],
  "selectedCurrency": "IDR",
  "checkin": "2026-11-01",
  "checkout": "2026-11-02",
  "lang": "en-us",
  "enableDetail": true,
  "bargain": false
}
```

#### Cross-currency planning

Scan the same destination in a different currency:

```json
{
  "destination": "Tokyo",
  "checkin": "2026-12-01",
  "checkout": "2026-12-05",
  "selectedCurrency": "JPY",
  "maxResults": 100,
  "enableDetail": false,
  "bargain": true
}
```

### Troubleshooting

| Symptom | Cause | Fix |
|---|---|---|
| Row shows `PARTIAL` | Page returned but could not be fully parsed | Rerun in the Apify Console (proxy enabled). Check the destination/country is reachable. |
| Row shows `BLOCKED` | Booking.com refused the request (WAF / rate limit) | Ensure `proxy.apifyProxyGroups` is `["RESIDENTIAL"]` (the default). Datacenter/AUTO traffic is blocked by Booking.com's WAF. |
| Row shows `NOT_FOUND` | Hotel page no longer exists or URL is wrong | Verify the `/hotel/{cc}/{slug}.html` URL is live — best source is the `url` field of a prior search run, not a hand-typed slug. |
| Run fails with `Invalid search URL` | `searchUrl` is not a Booking search URL | Use `destination` instead (e.g. `"destination": "Bandung"`) — the Actor builds the URL for you. Any `searchresults[.lang].html` variant is accepted for `searchUrl`. |
| Row shows `ERROR` | Server error or unexpected failure | Check `httpStatus` / `error`; retry after a moment. |
| `bargain` is `false` everywhere | Cohort median could not be computed (< 3 priced results) | Run with a broader search or a destination with more hotels. |
| Prices look like landing-page rates, not your dates | `priceBasis` is `city_indicative` | Provide a `checkin`/`checkout` so prices are quoted for your stay. |
| Local run returns partial/unreliable data | Running without a proxy | Run in the Apify Console where the Residential proxy is enabled. |

### FAQ

**Q: How much does it cost?**
A: $0.002 per hotel result, plus $0.006 per hotel detail enrichment when `enableDetail` is on. A 25-hotel search without detail costs about $0.05 before platform usage.

**Q: How fast is it?**
A: It's HTTP-first, so it's fast and light — a small run completes in about a minute. Large runs (thousands of hotels with detail enrichment) take longer and cost more via the detail add-on.

**Q: Can I scrape private or protected content?**
A: No. Only public Booking.com pages. You are responsible for complying with Booking.com's terms and applicable privacy/consumer law.

**Q: Is this legal? / What about the Booking.com ToS?**
A: You are responsible for the legality of your use in your jurisdiction and for respecting Booking.com's terms of service. This Actor only accesses public pages.

**Q: Can I run it on a schedule?**
A: Yes — the Apify Console supports scheduled runs. This also powers the cross-run price history: each scheduled rerun compares prices against the 90-day history.

**Q: Can I download the results?**
A: Yes — export the dataset as JSON, CSV, or Excel from the Apify Console, or pipe it to a webhook.

**Q: Can I copy or republish this Actor?**
A: No. This Actor is proprietary. You may run it on Apify. You may not rebrand, republish, or claim authorship. See [LICENSE](./LICENSE).

### Limitations

- Public data only. You are responsible for ToS and privacy law.
- Prices that cannot be quoted for your exact dates are marked `priceBasis: city_indicative` — indicative nightly rates, not stay quotes.
- `bargain` requires ≥ 3 priced results in the same destination cohort to compute a median.
- Booking.com's WAF can block datacenter traffic; if you hit `BLOCKED` rows, use the residential proxy opt-in (more expensive).
- Detail fields (`address`, `amenities`, `location`, `distance`) come from the optional detail pass; they are `null` or empty when disabled or unavailable.

### Run it

Open the Actor on Apify Console and click **Start**.

### License

**Proprietary. All rights reserved.** Copyright (c) 2026 REXREUS.

You may run this Actor on Apify. You may not copy, rebrand, republish, or claim this Actor as your own. See [LICENSE](./LICENSE).

# Actor input Schema

## `destination` (type: `string`):

City or area name, e.g. "Bali", "Bandung". Easiest input — builds the search URL for you. Ignored when searchUrl is set.

## `searchUrl` (type: `string`):

Full https://www.booking.com/searchresults... URL. Any searchresults variant accepted. Overrides destination if both set.

## `hotelUrls` (type: `array`):

https://www.booking.com/hotel/{cc}/{slug}.html URLs to scrape directly.

## `maxResults` (type: `integer`):

Maximum number of results (1..5000).

## `selectedCurrency` (type: `string`):

Currency for hotel prices.

## `checkin` (type: `string`):

YYYY-MM-DD. Overrides checkin in searchUrl.

## `checkout` (type: `string`):

YYYY-MM-DD. Overrides checkout in searchUrl.

## `lang` (type: `string`):

Language/locale tag, e.g. en-us, id-id, de.

## `enableDetail` (type: `boolean`):

Fetch each hotel's /hotel/ page for JSON-LD details (address, rating, amenities). Adds $0.006/result.

## `bargain` (type: `boolean`):

Flag price <= destination cohort median (needs >=3 priced results).

## `proxy` (type: `object`):

Proxy configuration (datacenter by default, residential opt-in). Requires plan access.

## Actor input object example

```json
{
  "maxResults": 100,
  "selectedCurrency": "USD",
  "lang": "en-us",
  "enableDetail": true,
  "bargain": false,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

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

Scraped hotel rows (search results and/or detail enrichment).

# 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 = {
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("rexreus/booking-com-hotel-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 = { "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    } }

# Run the Actor and wait for it to finish
run = client.actor("rexreus/booking-com-hotel-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 '{
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call rexreus/booking-com-hotel-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,rexreus/booking-com-hotel-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/VJRUF7cuKthSM3JpF/builds/fo0CBistXTk4D2jZ8/openapi.json
