# Booking.com Scraper - Hotels, Prices, Rooms & Reviews (`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
- **Features:** Limited permissions
- **Stats:** 1 total users, 0 monthly users, 80.6% runs succeeded, 1 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?

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

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

## How to integrate an Actor?

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

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

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

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

# README

## Booking.com Scraper: Hotel Prices, Rooms and Guest Reviews

Booking.com Scraper turns booking.com into structured accommodation data. Search a destination and a stay, or paste links you already built on the site, and get back one row per property with a live price in the currency you choose, the guest review score, the exact location, the matched room configuration and, when you turn it on, the whole property page plus its guest reviews. It covers hotels, apartments, villas, hostels, guesthouses, bed and breakfasts, campgrounds and every other kind of place the site lists. Export to JSON, CSV or Excel, or pull the results straight into your app through the API.

### Why This Scraper?

- **Two ways to find data.** Name a destination and let the actor search, or paste links you already built (a search results page or one property) and get exactly what you saw.
- **Real prices for real dates.** Every price is for the stay you asked for, converted into one of 36 currencies, with the property's own currency returned alongside it so nothing is hidden behind an exchange rate.
- **The site's own filters, plus three it does not enforce itself.** Type, star rating, review score, budget, meals, cancellation policy, amenities, bed type and travel group are all sent as the site's own filter tokens. Minimum review count, sold out status and actual availability for your dates are checked by the actor afterward, because the site's own tokens for those do not narrow anything.
- **Property depth on demand.** Turn on the property's own page for facilities, house rules, the room catalogue, policies and photos, and turn on guest reviews separately. The two share one per-property surcharge, billed only for returned properties where the extra read returned data. Reviews are on by default (20 per property); set them to `0` to skip the surcharge.
- **Built for monitoring.** Incremental mode returns only new and changed properties on a schedule, and resume continues an interrupted run without re-collecting what it already has.
- **Honest about the site's own limits.** A destination bigger than the site will serve anonymously is reported as such in the log, not silently truncated with no explanation.

### Use Cases

- **Travel search and metasearch:** build a comparison feed of live prices, availability and review scores across many destinations.
- **Revenue management and competitor pricing:** track a named set of properties for price and availability changes on a recurring schedule.
- **Review and reputation analysis:** pull guest review text, category scores and traveller types to summarize what guests actually say about a property.
- **Booking assistants and AI agents:** look up a destination or a specific property on demand and pipe structured results into your own tool or MCP client.
- **Market research:** profile the accommodation mix (property types, star ratings, amenities) of a destination.

### Data You Get

> Sample shape: values are illustrative placeholders, not from a live record.

| Field | Example |
| --- | --- |
| `propertyId` / `propertyName` | `"0000000"` / `"Example Harbour Hotel"` |
| `propertyUrl` / `pageName` | link to the property page / `"example-harbour-hotel"` |
| `accommodationTypeId` / `hostType` | `999` (a type code) / `"PROFESSIONAL"` (the host label the site shows, when it shows one) |
| `starRating` / `starRatingSymbol` | `4` / `"STARS"` |
| `reviewScore` / `reviewScoreLabel` / `reviewCount` | `9.2` / `"Wonderful"` / `1348` |
| `secondaryReviewScore` / `externalReviewScore` | a second, source specific score / a score from an outside review site (when present) |
| `address` / `city` / `countryCode` | `"1 Example Street"` / `"Sydney"` / `"au"` |
| `latitude` / `longitude` | `-33.9` / `151.1` |
| `displayLocation` / `distanceFromSearchCentre` | `"Sydney, Australia"` / `"1.2 km from centre"` |
| `mainPhoto` / `thumbnailPhoto` | property photo URLs |
| `price.perStay` / `price.perNight` / `price.currency` | `310.0` / `155.0` / `"USD"` |
| `price.propertyCurrency` / `price.perStayInPropertyCurrency` | `"AUD"` / `465.0` |
| `order` | `3` (the property's 1-based position in the site's own result list for that search; empty for a pasted property link) |
| `rooms[]` | one priced room offer each: `roomId`, `price`, `mealPlan`, `occupancy`, `freeCancellationUntil`, `urgencyMessage`; the **Room offers** output view shows them one per row |
| `matchedUnit` | bedrooms, bathrooms, beds, kitchens, living rooms, floor area |
| `freeCancellation` / `noPrepayment` / `petsAllowedFreeOfCharge` | `true` / `false` / `false` |
| `isPreferredPartner` / `hasGeniusRate` / `isSustainable` | `false` / `true` / `false` |
| `isSoldOut` / `isAvailableForDates` | `false` / `true` |
| `checkIn` / `checkOut` / `nights` / `adults` / `children` / `roomsRequested` | the stay actually used for this row |
| `description` / `facilityGroups` / `topFacilities` / `houseRules` / `roomTypes` | the property's own page (only with `fetchDetails` on) |
| `childrenAndBeds` / `petsPolicy` / `damagePolicy` / `finePrint` / `photos` | more property page fields (only with `fetchDetails` on) |
| `reviews[]` | score, title, what the guest liked and did not, language, the stay, the traveller type, the property's own reply, and the reviewer's public display name and country (only with `maxReviewsPerProperty` above 0) |
| `changeType` / `changedFields` / `firstSeenAt` / `lastSeenAt` | incremental mode only: `"UPDATED"` / `["price"]` / timestamps |

Every row is one property. Guest reviews live inside that same row as `reviews[]`; they are never a separate record.

### How to Use

1. Pick a **mode**: `search` (name one or more destinations) or `url` (paste booking.com links).
2. In search mode, name your destinations and set the stay (check-in, check-out, adults, rooms, currency), then narrow with the site's own filters.
3. Turn on **Read each property's own page** and/or set **Guest reviews per property** if you want the property's full detail and its reviews.
4. Set **Max results** to control run size and cost, then click **Start**.

**Search a destination with 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
}
```

**Paste links (a search page and one property), with full detail and reviews:**

```json
{
  "mode": "url",
  "urls": [
    "https://www.booking.com/searchresults.html?ss=Example+Town&checkin=2026-11-02&checkout=2026-11-04",
    "https://www.booking.com/hotel/au/example-harbour-hotel.html"
  ],
  "fetchDetails": true,
  "maxReviewsPerProperty": 50
}
```

#### URL mode: accepted links

| Link shape | Example | What it does |
|---|---|---|
| Property | `https://www.booking.com/hotel/au/hilton-sydney.html` (any `.en-gb`-style suffix, `www.` or `m.` host) | Returns that one property |
| Search | `https://www.booking.com/searchresults.html?ss=Sydney&checkin=2026-12-01&checkout=2026-12-03&group_adults=2` | Walks the search with the dates, guests, `dest_id`, `nflt` filters, `order` and `offset` the link carries |
| City | `https://www.booking.com/city/au/sydney.html` | Walked as a search for the named city |
| Region | `https://www.booking.com/region/au/new-south-wales.html` | Walked as a search for the named region |
| District | `https://www.booking.com/district/au/sydney/the-rocks.html` | Walked as a search for the named district |
| Landmark | `https://www.booking.com/landmark/au/sydney-opera-house.html` | Walked as a search for properties near the landmark |
| Country | `https://www.booking.com/country/au.html` (also `au.en-gb.html`) | Walked as a search for the named country |

A link in none of these shapes is skipped with a warning that names it. **Default behaviour change:** in `url` mode, running with the `urls` field untouched now reads one example of each shape above, instead of a single property. **Max results** is shared across the links: pasted property links are read first, then each search or landing-page link gets an equal share of what is left of the cap (with the default of 20 that is about 4 properties per link), so a large city link cannot use up the cap before the others are reached. Raise **Max results** to read more from each link. Clear the field and paste your own links to scrape only those.

**Monitor a destination on a schedule:**

```json
{
  "mode": "search",
  "destinations": ["Sydney"],
  "checkIn": "2026-11-02",
  "checkOut": "2026-11-04",
  "incrementalMode": true,
  "stateKey": "sydney-watch",
  "emitExpired": true,
  "maxItems": 0
}
```

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/booking-com-scraper").call(run_input={
    "mode": "search", "destinations": ["Sydney"],
    "checkIn": "2026-11-02", "checkOut": "2026-11-04"
})
for prop in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(prop["propertyName"], prop["price"]["perNight"])
```

JavaScript:

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

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('abotapi/booking-com-scraper').call({
  mode: 'search', destinations: ['Sydney'],
  checkIn: '2026-11-02', checkOut: '2026-11-04',
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

Or connect it to Make, Zapier, n8n, Google Sheets or webhooks from the **Integrations** tab.

#### How Max results shapes a run

With several destinations, Max results is split evenly across them so one destination cannot fill the whole run on its own. Each destination is then walked page by page, most relevant first unless you change **Sort by**, until its own share is filled or the source runs out. One search cannot go past 1000 properties; the site stops serving new results there regardless of the total it declares, and the run says so in the log. Set `sortBy` to `price_low_to_high` or `rating_high_to_low` when the first N matter, and split a very large destination by neighbourhood, property type, star rating or budget band to reach further into it.

Two things worth knowing about how Max results interacts with the other run modes:

- **Max results is spent on properties read, not properties returned, whenever something drops rows after the read.** Each destination stops reading pages once it has read its share, and only then are rows dropped: unchanged properties on an incremental run, and properties that fail the three checks the actor applies itself (**Minimum number of reviews**, **Drop sold-out properties**, **Only properties available for these dates**). So a run where most of the first page is unchanged, or fails one of those checks, can return well under Max results even though qualifying properties further down the result list were never reached. If a run returns fewer rows than expected, raise Max results well above what you expect to receive, or set it to `0`.
- **With more than one destination or pasted search link in the same run, a property that appears in two overlapping searches can be silently lost from both.** A destination reads a whole page (25 properties) before checking whether its own share is full, and every property on that page is recorded as handled, including the ones past the share that it then does not return. A later destination that finds those same properties skips them as already handled, even when it has room left, so they are returned by neither. If your destinations or links are likely to overlap (neighbouring searches, or the same city under two names), run them as separate actor runs instead of combining them in one.

#### Resume and recurring updates

- **Resume** (`resumeFromRunId`) continues one interrupted run: paste its run or dataset ID and the actor skips every property that run already collected, so you are not charged for it twice.
- **Incremental mode** (`incrementalMode`) is for scheduled runs against the same search or links. Each property is classified `NEW`, `UPDATED` (with a `changedFields` list), `UNCHANGED` (suppressed and not billed unless `emitUnchanged` is on), `REAPPEARED` or `EXPIRED`. `EXPIRED` is only emitted (with `emitExpired` on) after a run that reached the natural end of every destination or search link with nothing capped, resumed or refused, so a partial search never marks a live property as gone (see the exception for pasted property links below). `stateKey` names or shares the stored state; leave it empty and the actor derives one from your search or links. With incremental mode off, output is exactly as before.

Three behaviors worth knowing before you rely on incremental mode:

- **An `emitExpired` run does not cap the number of EXPIRED rows at Max results.** Once a run is complete enough to mark properties EXPIRED at all, every tracked property that was not found this time is returned, however many that is. On a large tracked destination where many properties disappeared at once, this can return and bill far more EXPIRED rows than the Max results you set for the run.
- **In `url` mode, a pasted property link that could not be read is treated as gone.** Unlike a destination or a pasted search link, a refused property link does not stop the run from counting as complete, so with `emitExpired` on that property is returned (and billed) as EXPIRED, then returned (and billed) again as REAPPEARED on the next run that reads it. The log still warns that some requests could not be read. If you track individual property links, leave `emitExpired` off, or check that warning before acting on EXPIRED rows.
- **Starting an incremental campaign with Resume re-bills the resumed properties as UPDATED once, on the next ordinary run, even if nothing about them changed.** Resume is only accepted on a campaign with no saved state yet. It seeds the properties it skips into the new baseline without a comparable fingerprint, purely so that run does not wrongly mark them EXPIRED. The next scheduled run that actually re-reads them cannot tell that placeholder apart from a real change, so it reports each of them as UPDATED with every field listed as changed. It settles after that one run.

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

Optionally pipe the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. This is an extra delivery step **after** the scrape: the Apify dataset is never changed.

**What gets written to the connector:** a condensed, human-readable **summary** of each property, not the full JSON. Each property becomes one entry with a **title** and its key fields flattened to plain text. The **complete record always stays in the Apify dataset**.

1. Authorize a connector once under **Apify → Settings → Integrations** (Notion, Linear, Airtable, or Apify).
2. Select it in the **"MCP connectors"** input field. (If the picker is empty, you haven't authorized a connector yet.)
3. For **Notion**, also set `notionParentPageUrl` to the page where property pages should be created.
4. Use `maxNotifyProperties` to cap how many properties are pushed per run; the dataset always keeps every one of them.

The connection is mediated by Apify's MCP proxy, so this actor never sees your third-party credentials. Leave the field empty to skip.

### Input Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `mode` | string | `search` | `search` (name destinations) or `url` (paste booking.com links). |
| `destinations` | array | sample | Destinations to search, one per line, exactly as typed into the site's own search box. |
| `propertyTypes` | array | `[]` | Keep only these kinds of place. Several widen the search. Search mode only. |
| `starRatings` | array | `[]` | Keep only these star ratings. Several widen the search. |
| `minReviewScore` | string | `any` | Minimum guest review score out of 10. |
| `minPricePerNight` | integer | (none) | Lower bound of the nightly budget, in the currency below. |
| `maxPricePerNight` | integer | (none) | Upper bound of the nightly budget, in the currency below. |
| `mealPlans` | array | `[]` | Keep only properties offering ALL of the picked meal arrangements. |
| `reservationPolicies` | array | `[]` | Keep only properties matching ALL of the picked booking conditions. |
| `amenities` | array | `[]` | Keep only properties with ALL of the picked property amenities. |
| `roomAmenities` | array | `[]` | Keep only properties whose rooms have ALL of the picked amenities. |
| `bedPreferences` | array | `[]` | Keep only properties offering these bed types. Several widen the search. |
| `travelGroups` | array | `[]` | Keep only properties suited to ALL of the picked traveller types. |
| `sortBy` | string | `relevance` | Result order (also guest review score then price, and star rating then price), which decides WHICH properties you get when Max results stops the run early. Search mode only. |
| `urls` | array | one example per shape | One booking.com link per line: a property, search, city, region, district, landmark or country link. See the URL mode table. |
| `checkIn` | string | (none) | `YYYY-MM-DD`. Left empty, prices a 2 night stay 30 days out and says so in the log. |
| `checkOut` | string | (none) | `YYYY-MM-DD`, after check-in. Left empty, becomes 2 nights after check-in. |
| `adults` | integer | `2` | Adults in the party. |
| `childrenAges` | array | (none) | One age (0 to 17) per child, one per line. |
| `rooms` | integer | `1` | Rooms the party needs. |
| `currency` | string | `USD` | Currency for every price in the output and for the budget filters above. |
| `language` | string | `en-us` | Language for names, room names, facility labels and review text where translated. |
| `availableOnly` | boolean | `false` | Drop properties not actually bookable for your dates. Applied by the actor after the search. |
| `minReviewCount` | integer | (none) | Drop properties with fewer reviews than this. Applied by the actor after the search. |
| `excludeSoldOut` | boolean | `false` | Drop properties the site has explicitly flagged sold out. Applied by the actor after the search. |
| `fetchDetails` | boolean | `false` | Read each property's own page: facilities, house rules, policies, room catalogue, photos. Adds the per-property surcharge. |
| `maxReviewsPerProperty` | integer | `20` | Guest reviews to attach per property, newest first. `0` returns none. Adds the per-property surcharge. |
| `maxItems` | integer | `20` | Stop after this many properties. `0` means no limit (up to 1000 per search). |
| `maxPages` | integer | `0` | Stop each destination after this many result pages. `0` walks until results run out. |
| `resumeFromRunId` | string | (none) | Continue one interrupted run, skipping what it already collected. |
| `incrementalMode` | boolean | `false` | Return only NEW, UPDATED and REAPPEARED properties on a scheduled run over the same search. |
| `stateKey` | string | (none) | Name this monitoring campaign. Leave empty to derive one from the search or links. |
| `emitUnchanged` | boolean | `false` | Incremental mode only. Also return (and bill) properties that did not change. |
| `emitExpired` | boolean | `false` | Incremental mode only. Also return (and bill) a row for each tracked property no longer found. |
| `ignoreFieldsForChanges` | array | (none) | Incremental mode only: field names (for example `reviewScore`) whose changes do not count; a property differing only in them is treated as unchanged and not billed. Matches at any nesting depth. |
| `proxy` | object | Apify Proxy | Connection settings. |
| `mcpConnectors` | array | (none) | Optional: send a summary of each property to apps you authorized under Integrations. |
| `notionParentPageUrl` | string | (none) | Notion connector only: page under which property pages are created. |
| `maxNotifyProperties` | integer | `50` | Cap on properties written to each connector per run. |
| `maxNotifyListings` | integer | (none) | Same cap under its other name; when set it takes precedence over `maxNotifyProperties`. |

### Output Example

> Sample shape: values are illustrative placeholders, not from a live record.

```json
{
  "propertyId": "0000000",
  "propertyName": "Example Harbour Hotel",
  "propertyUrl": "https://www.booking.com/hotel/au/example-harbour-hotel.html",
  "pageName": "example-harbour-hotel",
  "accommodationTypeId": 999,
  "destinationId": -1,
  "starRating": 4,
  "starRatingSymbol": "STARS",
  "reviewScore": 9.2,
  "reviewScoreLabel": "Wonderful",
  "reviewCount": 1348,
  "address": "1 Example Street",
  "city": "Sydney",
  "countryCode": "au",
  "latitude": -33.9,
  "longitude": 151.1,
  "displayLocation": "Sydney, Australia",
  "price": {
    "currency": "USD",
    "perStay": 310.0,
    "perNight": 155.0,
    "perStayFormatted": "$310.00",
    "beforeDiscount": 360.0,
    "chargesInfo": "Includes taxes and fees",
    "propertyCurrency": "AUD",
    "perStayInPropertyCurrency": 465.0
  },
  "rooms": [
    { "roomId": "0000010", "occupancy": 2, "price": 310.0, "currency": "USD",
      "mealPlan": "Breakfast included", "freeCancellationUntil": "2026-10-30" }
  ],
  "matchedUnit": { "bedrooms": 1, "bathrooms": 1, "beds": 1,
                   "area": { "value": 30, "unit": "m2" } },
  "freeCancellation": true,
  "noPrepayment": false,
  "petsAllowedFreeOfCharge": false,
  "isSoldOut": false,
  "isAvailableForDates": true,
  "checkIn": "2026-11-02",
  "checkOut": "2026-11-04",
  "nights": 2,
  "adults": 2,
  "children": 0,
  "roomsRequested": 1,
  "houseRules": { "checkInFrom": "2:00 PM", "checkOutUntil": "10:00 AM" },
  "reviews": [
    { "score": 8.0, "title": "Great stay",
      "positiveText": "Comfortable room and helpful staff",
      "negativeText": "Breakfast area was busy", "reviewerCountry": "Canada",
      "travellerType": "FAMILIES", "nights": 2, "reviewedAt": "2026-09-01T00:00:00Z" }
  ],
  "searchMode": "search",
  "searchQuery": "Sydney",
  "scrapedAt": "2026-09-10T12:00:00Z"
}
```

### Plan Requirement

Apify datacenter proxies are enough for this actor; residential proxies are not required. The default proxy setting is used, and the actor rotates connections automatically when one is refused. On an Apify free plan, which includes no Apify proxy, the actor continues on a backup connection after the first refusal and says so in the log (some pages may still be unavailable); if no page can be read the run stops with a clear message instead of returning an empty dataset: either that the proxy connection was refused for the account (a paid plan or your own proxy URLs are needed), or that the backup connection could not load the pages. If only some requests are refused, you keep what was found and the log warns that the results may be incomplete. The **Pricing** tab on the actor page shows what a run costs.

### FAQ

#### How much does it cost?

You pay a fixed actor start fee, then per property returned, and once more for each returned property whose extra pages were read and returned data. The surcharge is the registered event **Property page or guest reviews read** (`detail-enrichment`): one unit per returned property whose property page or guest reviews were read and returned data, recorded property by property right after that property is added to the results, so a run that stops early is billed only for what it returned. The property page and its reviews share ONE per-property surcharge, so turning on both costs the same surcharge as turning on one. Guest reviews are on by default (20 per property), so set **Guest reviews per property** to `0` if you only need the result-page fields. A pasted property link always carries the surcharge, because its own page is the whole record. The **Pricing** tab shows the current rates. Use **Max results** to cap the cost of any run.

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

This actor collects data that booking.com publishes to anyone, without logging in, and it does not collect personal contact details. You are responsible for how you use the results: follow booking.com's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution. Guest review text and public reviewer display names are user generated content published by the site.

#### Can I get only new or changed properties on a schedule?

Yes. Schedule the actor from the **Schedules** tab and turn on **Incremental mode**. Each run then returns only new, updated and reappeared properties, with a `changedFields` list, and unchanged ones are not billed unless you turn that on too. Some fields are still returned but never count as a change on their own, because they move without the property changing: the search position (`order`), the long property description, the wishlist counter, the per-night price (it is the per-stay price divided by the nights) and display tags (`wishlistCount`, `topFacilities`, `starRatingSymbol`, `searchUrl`, `scrapedAt`), and the two wheelchair-accessibility facility tags, which the site shows on one read of a property page and drops or moves on the next. Add anything else you do not want to count to **Ignore fields when detecting changes (incremental mode only)**.

#### Why did a run return fewer properties than Max results?

Three common reasons. A destination bigger than 1000 properties stops there regardless of what it declares as its total; narrow the search to reach further into it. Max results is spent on properties read from the source, not properties actually returned, so a run where many rows are dropped afterward (unchanged on an incremental run, or failing the minimum review count, sold-out or availability checks) can return well under Max results even though qualifying properties exist further down the list; raise Max results, or set it to `0`. And with several overlapping destinations or search links in one run, a property one of them read past its own share can be skipped by the others; run overlapping searches separately.

#### Why did some properties come back as UPDATED right after I used Resume?

When Resume starts a new incremental campaign, it seeds the properties it skipped into the baseline without a real fingerprint, so the next ordinary scheduled run that actually re-reads them cannot tell that from a genuine change and reports each of them as UPDATED with every field listed as changed. It happens once, on that next run, and settles back to normal after that.

#### Why did my run fail instead of returning an empty dataset?

If the site refuses every request, the run stops with a clear message so "no properties matched" is never confused with "nothing could be read". Run it again in a few minutes. If the site does not know a destination you typed, the run stops with a message naming it ("No destination matched ..."): check the spelling or use a nearby city, region, landmark or airport name as the site's own search box shows it. If the run reaches its time limit before any page loaded, it stops with a message saying so; with rows already collected it keeps them and the log says the results are incomplete. If only some destinations or links could be read, you still get what was found, with a warning that the results may be incomplete.

#### Can I use it with AI agents or MCP?

Yes. Call it from any Apify integration or MCP client, and use the connector field to push results into Notion, Linear or Airtable.

### 🔗 Want more travel data?

Pair this actor with these related scrapers from the same team:

<table>
<tr><td>🏨 <a href="https://apify.com/abotapi/expedia-universal-scraper"><b>Expedia Hotels Scraper</b></a><br>Scrape Expedia.com hotel listings and reviews for any destination. Extract prices...</td><td>🏨 <a href="https://apify.com/abotapi/trip-com-scraper"><b>Trip.com Hotels, Rooms &amp; Reviews Scraper</b></a><br>Pull structured hotel listings from trip.com with prices, room types, amenities...</td></tr>
<tr><td>🏨 <a href="https://apify.com/abotapi/hostelworld-scraper"><b>Hostelworld Scraper</b></a><br>Scrape Hostelworld hostels, hotels, guesthouses, B&amp;Bs, apartments and campsites by city...</td><td>🏨 <a href="https://apify.com/abotapi/rakuten-travel-scraper"><b>Rakuten Travel Scraper</b></a><br>Scrape Rakuten Travel, Japan's largest domestic hotel-booking site, by prefecture...</td></tr>
<tr><td>🏨 <a href="https://apify.com/abotapi/yanolja-com-scraper"><b>Yanolja Scraper</b></a><br>Scrape Yanolja (NOL), Korea's largest accommodations marketplace, by region or URL. Every...</td><td>🏨 <a href="https://apify.com/abotapi/klook-activities-scraper"><b>Klook Scraper</b></a><br>Scrape Klook activities, tours, attractions and travel experiences from search pages or...</td></tr>
</table>

👉 [Browse all abotapi scrapers](https://apify.com/abotapi)

### 💬 Support

- 🐞 **Found a bug or a missing field?** Open a ticket on the [Issues tab](https://apify.com/abotapi/booking-com-scraper/issues/open). We usually reply within hours.
- 🛠️ **Need another site, extra fields or a private build?** Email <contact@abotapi.com>.
- ⭐ **Enjoying it?** A quick review on the actor page helps other users find it.

# 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.

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

One link per line. Accepted: a search link (searchresults.html) walked with the destination, dates, guests and filters it carries; a property link (/hotel/<country>/<name>.html, any locale suffix, www or m. host); a landing page (/city/, /region/, /district/, /landmark/, /country/) walked as a search for the place it names. Left untouched, one example of each shape is read; Max results is shared across the links (property links first, then an equal share per search or landing-page link).

## `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.

## `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.

## `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.

## `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.

## `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.

## `ignoreFieldsForChanges` (type: `array`):

Optional. Field names that must NOT count as a change in incremental mode, for example "reviewScore" or "price". A property whose only differences are in these fields is treated as UNCHANGED, so it is not returned or billed as UPDATED. Names match at any nesting depth. The fields still appear in the output of rows that are returned.

## `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.

## `maxNotifyListings` (type: `integer`):

Same cap as 'Max properties to export'. When set, it takes precedence. The dataset always keeps every row.

## Actor input object example

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

# Actor output Schema

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

No description

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

No description

## `roomOffers` (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",
        "https://www.booking.com/city/au/sydney.html",
        "https://www.booking.com/region/au/new-south-wales.html",
        "https://www.booking.com/district/au/sydney/the-rocks.html",
        "https://www.booking.com/landmark/au/sydney-opera-house.html",
        "https://www.booking.com/country/au.html"
    ],
    "ignoreFieldsForChanges": []
};

// 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",
        "https://www.booking.com/city/au/sydney.html",
        "https://www.booking.com/region/au/new-south-wales.html",
        "https://www.booking.com/district/au/sydney/the-rocks.html",
        "https://www.booking.com/landmark/au/sydney-opera-house.html",
        "https://www.booking.com/country/au.html",
    ],
    "ignoreFieldsForChanges": [],
}

# 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",
    "https://www.booking.com/city/au/sydney.html",
    "https://www.booking.com/region/au/new-south-wales.html",
    "https://www.booking.com/district/au/sydney/the-rocks.html",
    "https://www.booking.com/landmark/au/sydney-opera-house.html",
    "https://www.booking.com/country/au.html"
  ],
  "ignoreFieldsForChanges": []
}' |
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/AgcwH0nHeV2kukGQn/openapi.json
