# Booking.com Hotels, Prices and Reviews Scraper (`abotapi/booking-com-scraper`) Actor

Scrape booking.com hotels, apartments, villas and hostels. Search any destination with the site's own filters (type, stars, review score, budget, meals, cancellation, amenities) or paste links. Live prices in 36 currencies, room-level rates, facilities, house rules and guest reviews.

- **URL**: https://apify.com/abotapi/booking-com-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Categories:** Travel
- **Stats:** 2 total users, 1 monthly users, 80.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.20 / 1,000 properties

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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 Hotels, Prices and Reviews Scraper

Scrape accommodation from booking.com at scale: hotels, apartments, villas, hostels, guesthouses,
B\&Bs, campgrounds and everything else the site lists. Name a destination and a stay, or paste links
you already built in your own browser, and get back one structured row per property with a live
price, the rating, the exact location, the matched room configuration and, optionally, the whole
property page and its guest reviews.

Built for people who need the numbers to be right: prices in the currency you asked for, a review
score next to the number of reviews behind it, and an honest statement in the log when a destination
is bigger than the site will serve.

***

### What you get

**Every row, always**

| | |
|---|---|
| Identity | `propertyId`, `propertyName`, `propertyUrl`, `pageName`, `accommodationTypeId`, `hostType` |
| Rating | `starRating`, `reviewScore`, `reviewScoreLabel`, `reviewCount`, `secondaryReviewScore`, `externalReviewScore`, `externalReviewCount` |
| Location | `address`, `city`, `countryCode`, `latitude`, `longitude`, `displayLocation`, `distanceFromSearchCentre`, `distanceMetres`, `isCentrallyLocated`, `publicTransportDistance`, `beachDistance` |
| Price | `price.perStay`, `price.perNight`, `price.perStayFormatted`, `price.beforeDiscount`, `price.currency`, `price.chargesInfo`, `price.badges`, plus the property's own currency alongside |
| Rooms | `rooms[]` with `roomId`, `price`, `mealPlan`, `occupancy`, `freeCancellationUntil`, `urgencyMessage`; `matchedUnit` with bedrooms, bathrooms, beds, kitchens and floor area |
| Conditions | `freeCancellation`, `noPrepayment`, `petsAllowedFreeOfCharge`, `mealPlanIncluded`, `isAvailableForDates`, `isSoldOut`, `isSustainable`, `isPreferredPartner`, `hasGeniusRate` |
| Stay | `checkIn`, `checkOut`, `nights`, `adults`, `children`, `roomsRequested` |
| Photos | `mainPhoto`, `thumbnailPhoto` |

**With `fetchDetails` on, each row also carries the property's own page**

`facilityGroups` (every listed facility, grouped), `topFacilities`, `houseRules` (check-in and
check-out times, minimum check-in age, group limit, smoking, parties, city regulations, accepted
credit cards, cash), `childrenAndBeds` (with the per-age-band price rules), `petsPolicy`,
`damagePolicy`, `finePrint`, `photos` (up to 40), `roomTypes` (the full room catalogue with
amenities), `questionsAndAnswers`, `languagesSpoken`, `brands`, `chainIds`, `propertyType`,
`propertyCurrency`, `reviewCategoryScores` and `totalReviewCount`.

**With `maxReviewsPerProperty` above 0, each row also carries `reviews[]`**

Score, title, what the guest liked, what they did not, language, the date, the room they stayed in,
how many nights, the traveller type, the property's own reply, and the reviewer's public display
name and country. No contact details are collected: the review surface exposes none and this actor
invents none.

***

### Two ways to run it

#### Search mode

Name one or more destinations exactly as you would type them into the site's search box, set the
stay, and narrow with the site's own filters.

```json
{
  "mode": "search",
  "destinations": ["Sydney", "Lisbon"],
  "checkIn": "2026-11-02",
  "checkOut": "2026-11-04",
  "adults": 2,
  "rooms": 1,
  "currency": "USD",
  "starRatings": ["4", "5"],
  "minReviewScore": "8",
  "maxPricePerNight": 350,
  "reservationPolicies": ["free_cancellation"],
  "amenities": ["swimming_pool", "free_wifi"],
  "sortBy": "rating_high_to_low",
  "maxItems": 50
}
```

Filters available: property type, star rating, minimum guest review score, budget per night, meals,
reservation policy, property amenities, room amenities, bed preference and travel group. Those are
sent to the site as its own filter tokens, so they narrow the search itself.

Three more are applied by this actor after the search, because the site has no working filter for
them:

- `minReviewCount` drops a 10 out of 10 that came from two guests.
- `availableOnly` drops properties that are not bookable for your dates. The site pads a thin
  result set with these, and it does publish an availability filter, but it ignores it: sending
  that filter returns exactly the same result set as sending nothing. So this actor identifies the
  padded rows itself and reports the answer in the `isAvailableForDates` column, which is the same
  column the filter reads. Searching a small town for a large group is where this bites: one live
  check returned 25 cards of which only 2 were bookable.
- `excludeSoldOut` drops properties the site has explicitly flagged sold out. That is a narrower
  question than the one above, so it stays a separate option.

#### URL mode

Paste links. A search link is walked with the destination, dates, guests and filter string that link
already carries, so what you get is exactly what you saw. A property link returns that one property
with its full detail.

```json
{
  "mode": "url",
  "urls": [
    "https://www.booking.com/searchresults.html?ss=Lisbon&checkin=2026-11-02&checkout=2026-11-04&nflt=class%3D5",
    "https://www.booking.com/hotel/au/hilton-sydney.html"
  ],
  "fetchDetails": true,
  "maxReviewsPerProperty": 50
}
```

***

### Sample output

Illustrative shape, trimmed. Values are placeholders, not a real listing.

```json
{
  "propertyId": "0000000",
  "propertyName": "Example Harbour Hotel",
  "propertyUrl": "https://www.booking.com/hotel/au/example-harbour-hotel.html",
  "starRating": 4,
  "reviewScore": 8.6,
  "reviewScoreLabel": "Excellent",
  "reviewCount": 2793,
  "address": "1 Example Street",
  "city": "Sydney",
  "countryCode": "au",
  "latitude": -33.8712,
  "longitude": 151.207,
  "price": {
    "currency": "USD",
    "perStay": 412.5,
    "perNight": 206.25,
    "perStayFormatted": "$412.50",
    "beforeDiscount": 480.0,
    "chargesInfo": "Includes taxes and fees",
    "propertyCurrency": "AUD",
    "perStayInPropertyCurrency": 620.0
  },
  "rooms": [
    { "roomId": "0000001", "occupancy": 2, "price": 412.5, "currency": "USD",
      "mealPlan": "Breakfast included", "freeCancellationUntil": "2026-10-30" }
  ],
  "matchedUnit": { "bedrooms": 1, "bathrooms": 1, "beds": 1,
                   "area": { "value": 32, "unit": "m2" } },
  "freeCancellation": true,
  "checkIn": "2026-11-02",
  "checkOut": "2026-11-04",
  "nights": 2,
  "houseRules": { "checkInFrom": "3:00 PM", "checkOutUntil": "11:00 AM" },
  "reviews": [
    { "score": 9.0, "positiveText": "Great location and very clean",
      "negativeText": "Check-in was slow", "reviewerCountry": "Australia",
      "travellerType": "COUPLES", "nights": 3, "reviewedAt": "2026-08-25T00:00:00Z" }
  ],
  "scrapedAt": "2026-09-05T12:00:00Z"
}
```

***

### Things worth knowing before you run it

**Prices need a stay.** A price only exists for concrete dates, so if you leave `checkIn` and
`checkOut` empty the run prices a 2 night stay 30 days out and says which window it used in the log.
Set the dates whenever the dates matter.

**Currency is real conversion, not a label.** Every price comes back in the currency you picked, and
the property's own currency is returned beside it so you can see the original. The budget filter is
denominated in the same currency you picked, so the two can never disagree.

**One search reaches 1000 properties.** The site stops serving new results past that point, whatever
total it declares. A destination with 5000 properties will report 5000 and hand over 1000 of them.
The run says so in the log rather than pretending it finished. To reach the rest, split the search:
by neighbourhood link, by property type, by star rating or by budget band.

**Max results decides what you get, sort decides which.** With `maxItems` set and `sortBy` left on
the default, you get the site's own top picks. Set `sortBy` to `price_low_to_high` or
`rating_high_to_low` when the first N matter.

**Reviews are paginated.** `maxReviewsPerProperty` is read 25 at a time, so 100 reviews cost four
extra requests for that property. Reviews and the property page share ONE per-property surcharge, so
turning both on costs the surcharge once.

***

### Recurring runs

#### Continue an interrupted crawl

Put the previous run's id in `resumeFromRunId`. Everything that run already collected is skipped, so
you are not charged for it twice.

#### Monitor a search on a schedule

Turn on `incrementalMode`. The actor remembers the previous run of the same search by itself and
returns only what changed, each row tagged `changeType` (`NEW`, `UPDATED`, `REAPPEARED`) with a
`changedFields` list, `firstSeenAt` and `lastSeenAt`. Price moves, rating moves and availability
changes are what you will see most.

Set `stateKey` to run several independent campaigns. `emitUnchanged` and `emitExpired` add rows for
properties that did not change and for ones that disappeared; both are off by default and both bill
as results when on. `EXPIRED` is only ever emitted after a run that reached the natural end of every
search, so a capped, resumed or partly blocked run never marks a live property as gone.

***

### Send results into your apps (MCP connectors)

Optionally push the results straight into your own tools through Apify's MCP proxy. Set
`mcpConnectors` to the connectors you have authorised (for example `notion` or `slack`), set
`notionParentPageUrl` when you use Notion so pages are created in the right place, and use
`maxNotifyProperties` to cap how many properties are exported. The dataset always keeps everything;
the connector export is a side channel and never changes what was scraped.

***

### Output views

The dataset ships four views on the Output tab: **Properties** (price, rating, location, stay),
**Property detail** (facilities, house rules, room catalogue, photos), **Guest reviews** (scores and
review text) and **Changes since the last run** (incremental mode).

***

### What you are charged for

You are charged once when the run starts, once for each property returned, and once more for each
property whose extra pages (its own page, its reviews, or both) were read and returned data. A
property that is suppressed by incremental mode, or whose extra read failed, is not charged the
surcharge. A run that returns nothing has nothing to pay for beyond the start. The rates are on the
Store page for this actor.

### Legal

This actor collects data that booking.com publishes to anyone, without logging in. It does not
collect personal contact details. Review text and public reviewer display names are user generated
content published by the site; if you process them, do so in line with the terms of the site and
with the data-protection law that applies to you.

# Actor input Schema

## `mode` (type: `string`):

Search: name destinations and let the actor build the search. URL: paste booking.com search links or property links and walk exactly those.

## `destinations` (type: `array`):

One destination per line, exactly as you would type it into the site's own search box: a city, a region, a neighbourhood, a landmark or an airport. Example: Sydney, Lisbon, Manhattan New York.

## `propertyTypes` (type: `array`):

Keep only these kinds of place. Several kinds widen the search (any of them matches). Leave empty for every kind. Search mode only; pasted links keep their own filters.

## `starRatings` (type: `array`):

Keep only properties with these star ratings. Several ratings widen the search (any of them matches).

## `minReviewScore` (type: `string`):

Keep only properties scoring at least this, out of 10, from guest reviews.

## `minPricePerNight` (type: `integer`):

Lower bound of the nightly budget, in the currency selected below.

## `maxPricePerNight` (type: `integer`):

Upper bound of the nightly budget, in the currency selected below.

## `mealPlans` (type: `array`):

Keep only properties offering ALL of the meal arrangements you pick, so each extra one narrows the search further.

## `reservationPolicies` (type: `array`):

Keep only properties whose booking conditions match ALL of the ones you pick, so each extra one narrows the search further.

## `amenities` (type: `array`):

Keep only properties that have ALL of these, so each extra one narrows the search further.

## `roomAmenities` (type: `array`):

Keep only properties whose rooms have ALL of these, so each extra one narrows the search further.

## `bedPreferences` (type: `array`):

Keep only properties offering these bed types for your party. Several types widen the search (any of them matches).

## `travelGroups` (type: `array`):

Keep only properties suited to ALL of these travellers, so each extra one narrows the search further.

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

Which order the site returns results in. This decides WHICH properties you get when Max results stops the run early. Search mode only; a pasted link keeps its own order.

## `urls` (type: `array`):

One link per line. A search link (searchresults.html) is walked with the destination, dates, guests and filters that link already carries. A property link (/hotel/<country>/<name>.html) returns that one property with its full detail.

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

YYYY-MM-DD. Prices exist only for a real stay, so if you leave this empty the run prices a 2 night stay 30 days from now and says so in the log.

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

YYYY-MM-DD. Must be after check-in. Left empty, it becomes 2 nights after check-in.

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

Adults in the party. Changes which rooms and prices come back.

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

One age (0 to 17) per child, one per line. The number of children is taken from how many ages you list.

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

How many rooms the party needs.

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

Every price in the output is converted to this currency, and the budget filter above is denominated in it. The property's own currency is returned alongside it.

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

Language for property names, room names, facility labels and review text where the site has a translation.

## `availableOnly` (type: `boolean`):

The site pads a thin result set with places that are not actually bookable for your dates. Turn this on to drop them and keep only the bookable ones, reported in the isAvailableForDates column. Applied by this actor after the search, because the site ignores its own availability filter tokens. Works in search mode and for pasted search links; a pasted property link has no availability context to test.

## `minReviewCount` (type: `integer`):

Drop properties with fewer reviews than this. A 10 out of 10 from two guests is not the same evidence as an 8.9 from four thousand. Applied by this actor after the search, because the site's own filters bound the score but never the sample size.

## `excludeSoldOut` (type: `boolean`):

Applied by this actor after the search. Useful mainly for pasted links, whose own filter string may not carry the availability option.

## `fetchDetails` (type: `boolean`):

Adds facilities, house rules, check-in and check-out times, the child and bed policy, accepted payment methods, the fine print, the photo gallery, the room catalogue and guest questions. Costs one extra request per property and adds the per-property surcharge.

## `maxReviewsPerProperty` (type: `integer`):

How many guest reviews to attach to each property, newest first. 0 returns none. Reviews are read 25 at a time, so 100 costs four extra requests per property and adds the per-property surcharge.

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

Stop after this many properties. 0 means no limit, which on a big destination can be up to 1000 per search: the site stops serving new results past that point, whatever it declares as the total.

## `maxPages` (type: `integer`):

Stop each destination after this many result pages (25 properties per page). 0 walks until the results run out or the site stops serving new ones.

## `resumeFromRunId` (type: `string`):

Paste a run id or dataset id from an earlier run of this actor to continue where it stopped: everything that run already collected is skipped, so you are not charged for it twice. For recurring monitoring use Incremental mode below instead.

## `incrementalMode` (type: `boolean`):

For scheduled runs against the same search. The actor remembers the previous run itself and returns only NEW, UPDATED and REAPPEARED properties, with a changedFields list. Use Resume from a previous run above to continue one interrupted crawl instead.

## `stateKey` (type: `string`):

Name this monitoring campaign so several schedules can track different searches independently. Leave empty and the actor derives one from the mode, destinations, links, filters and stay.

## `emitUnchanged` (type: `boolean`):

Incremental mode only. Returns properties that did not change since the last run. These are extra rows and they are billed as results.

## `emitExpired` (type: `boolean`):

Incremental mode only. Returns a row marked EXPIRED for each tracked property no longer found, but only after a run that reached the natural end of every search. These are extra rows and they are billed as results.

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

Leave the default on. The shared connection pool is what this actor is tuned for and it costs far less than the traffic-billed one.

## `mcpConnectors` (type: `array`):

Optional. Send the results straight into your own apps through Apify's MCP proxy, for example notion or slack. Leave empty to skip.

## `notionParentPageUrl` (type: `string`):

Only used when the notion connector is selected: the page new property pages are created under.

## `maxNotifyProperties` (type: `integer`):

How many properties to push into the connectors above. The dataset always keeps them all.

## Actor input object example

```json
{
  "mode": "search",
  "destinations": [
    "Sydney"
  ],
  "propertyTypes": [],
  "starRatings": [],
  "minReviewScore": "any",
  "mealPlans": [],
  "reservationPolicies": [],
  "amenities": [],
  "roomAmenities": [],
  "bedPreferences": [],
  "travelGroups": [],
  "sortBy": "relevance",
  "urls": [
    "https://www.booking.com/hotel/au/hilton-sydney.html"
  ],
  "adults": 2,
  "rooms": 1,
  "currency": "USD",
  "language": "en-us",
  "availableOnly": false,
  "excludeSoldOut": false,
  "fetchDetails": false,
  "maxReviewsPerProperty": 20,
  "maxItems": 20,
  "maxPages": 0,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "proxy": {
    "useApifyProxy": true
  },
  "maxNotifyProperties": 50
}
```

# Actor output Schema

## `overview` (type: `string`):

No description

## `detail` (type: `string`):

No description

## `reviews` (type: `string`):

No description

## `changes` (type: `string`):

No description

# 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 = {
    "destinations": [
        "Sydney"
    ],
    "urls": [
        "https://www.booking.com/hotel/au/hilton-sydney.html"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/booking-com-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 = {
    "destinations": ["Sydney"],
    "urls": ["https://www.booking.com/hotel/au/hilton-sydney.html"],
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/booking-com-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 '{
  "destinations": [
    "Sydney"
  ],
  "urls": [
    "https://www.booking.com/hotel/au/hilton-sydney.html"
  ]
}' |
apify call abotapi/booking-com-scraper --silent --output-dataset

```

## MCP server setup

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