# Booking.com Scraper: Guest Review Sentiment Analysis (`scrapier/bookingscraper`) Actor

Booking.com Scraper: Extract guest reviews, ratings, dates, property details, and review text, then analyze sentiment to identify positive and negative feedback. Uncover recurring complaints, guest preferences, service issues, and hospitality trends for competitive research.

- **URL**: https://apify.com/scrapier/bookingscraper.md
- **Developed by:** [Scrapier](https://apify.com/scrapier) (community)
- **Categories:** Travel, Automation, Developer tools
- **Stats:** 9 total users, 5 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.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 — Hotels, Guest Reviews and Sentiment Analysis

Booking.com Scraper: Guest Review Sentiment Analysis takes a destination search or a list of Booking.com URLs and returns three linked result types in one dataset: the full hotel record, each property's guest reviews as child rows, and a keyless `reviewSentiment` rollup — positive/neutral/negative counts, recommend rate and a ranked aspect breakdown — computed per hotel with no API key and no login. Every response is structured JSON, ready to pass straight to an LLM, load into a spreadsheet, or feed a monitoring pipeline. Point it at a city, a landmark, or a hotel URL and get hotel data plus scored guest sentiment back from the same run.

### What is Booking.com Scraper: Guest Review Sentiment Analysis?

Booking.com Scraper: Guest Review Sentiment Analysis is an Apify Actor that queries Booking.com by destination (or a supplied list of hotel/search URLs) and returns hotel property records, each hotel's embedded guest reviews as linked child rows, and a computed sentiment/aspect summary per hotel — all as structured JSON or CSV. No Booking.com account, login, or API key is required for the core hotel, review and sentiment data; an API key is only needed if you turn on the optional LLM review-classification add-on. What sets it apart from a plain review scraper is that the `reviewSentiment` object is calculated directly from the scraped reviews' real 1–10 scores and liked/disliked text — not copied from a UI label — so every hotel row carries a ready-to-use reputation score without a separate analysis step.

- **Hotel property data** — name, stars, price, currency, guest rating, rooms, facilities, address, coordinates, house-rule policies, trader/host contact info and breadcrumbs.
- **Guest review child rows** — liked/disliked text, per-review score, review title, reviewer name and country, traveller type, trip purpose, room type and review date, linked to the parent hotel.
- **Keyless sentiment and aspect rollup** — positive/neutral/negative counts, recommend rate, average review score and a ranked list of aspects (staff, cleanliness, location, comfort, facilities, value, breakfast, food, room, Wi-Fi, noise) mentioned across the reviews.
- **Optional AI review classification** — a multi-provider LLM pass that adds `aiSentiment` and `aiTopics` per review when you supply your own API key.
- **Query controls** — destination text or start URLs, property type, star rating, minimum guest rating, sort order, currency, interface language, stay dates with flexible-window search, room/guest counts and a price range filter.
- **Two input modes** — a free-text destination search, or a pasted list of Booking.com hotel and search-results URLs, both supported in the same run.

### 🧠 What data can you get with Booking.com Scraper: Guest Review Sentiment Analysis?

The Actor pushes two row types to one dataset — hotel rows and review rows — plus a computed sentiment object attached to every hotel row.

| Result Type | Extracted Fields | Primary Use Case |
| --- | --- | --- |
| Hotel (parent row, `type: "hotel"`) | `name`, `stars`, `price`, `currency`, `rating`, `ratingLabel`, `reviews`, `address`, `location`, `rooms`, `facilities`, `policies`, `images`, `hotelChain`, `licenseInfo`, `hostInfo`, `traderInfo`, `breadcrumbs`, `hotelId`, `url` | Property comparison, pricing and availability research |
| Guest review (child row, `type: "review"`) | `reviewerName`, `reviewerCountry`, `travellerType`, `tripPurpose`, `reviewScore`, `sentiment`, `title`, `positiveText`, `negativeText`, `roomType`, `reviewedAt`, `reviewUrl`, `aiSentiment`, `aiTopics` | Guest feedback analysis, reputation tracking |
| Sentiment & aspect rollup (on every hotel row) | `reviewSentiment.reviewsAnalyzed`, `.positiveCount`, `.neutralCount`, `.negativeCount`, `.recommendRate`, `.averageReviewScore`, `.topAspects[]` | Aggregate reputation scoring without a separate analysis tool |

#### 🧠 Keyless sentiment and aspect rollup

`reviewSentiment` is attached to every hotel row when `analyzeSentiment` is on (default). It is built entirely from the reviews collected for that hotel — no third-party sentiment API, no keyword dictionary lookup service, no LLM call unless you separately enable `aiEnhancement`. Per-review polarity comes from `review_sentiment()`: a score ≥ 8 is `positive`, a score ≥ 6 is `neutral` (nudged to `negative` if the review has disliked text but no liked text), anything below 6 is `negative`; when a review has no numeric score at all, polarity falls back to whichever of `positiveText` / `negativeText` is present. The hotel-level rollup then counts polarities, computes a recommend rate (`positiveCount / judgedReviews`), averages the numeric scores, and scans every liked/disliked span against a fixed keyword vocabulary (cleanliness, staff, location, comfort, facilities, value, breakfast, food, room, Wi-Fi, noise) to rank the top 10 aspects by mention volume, each carrying a net polarity:

```json
"reviewSentiment": {
  "reviewsAnalyzed": 10,
  "positiveCount": 8,
  "neutralCount": 1,
  "negativeCount": 1,
  "recommendRate": 0.8,
  "averageReviewScore": 8.6,
  "topAspects": [
    { "aspect": "staff", "positiveMentions": 7, "negativeMentions": 1, "netSentiment": "positive" },
    { "aspect": "location", "positiveMentions": 6, "negativeMentions": 0, "netSentiment": "positive" },
    { "aspect": "room", "positiveMentions": 4, "negativeMentions": 2, "netSentiment": "positive" }
  ]
}
```

#### Guest review child rows

Each hotel's featured reviews — read straight out of the same page's embedded Apollo state used to build the hotel record, no second request — become their own dataset rows with `type: "review"`, `isChild: true` and `parentId` set to the hotel's `hotelId`. A review row carries the liked and disliked text separately, the reviewer's numeric score, traveller type ("Family with young children", "Business traveller", "Solo traveller"…), trip purpose, room type, and the review date normalized to ISO 8601. Reviews are also mirrored, uncharged, into a per-run `reviews-<runId>` named dataset for downstream jobs that only need the review rows.

### Why not build a Booking.com scraper yourself?

Booking.com does not expose a self-serve public API for hotel search or guest reviews — its Demand/Connectivity APIs are partner-gated and unsuitable for one-off destination research. Building this yourself means reverse-engineering the hotel page's embedded Apollo GraphQL cache (which changes shape between deploys), then getting past Booking.com's AWS WAF challenge, which blocks plain HTTP clients outright and requires a real browser to solve a JavaScript challenge and hold the resulting `aws-waf-token` cookie across requests. On top of that you need residential proxy capacity, retry/rotation logic for when the challenge doesn't clear, and your own sentiment scoring layer if you want anything beyond raw review text. Booking.com Scraper: Guest Review Sentiment Analysis already does all of that — Playwright-driven WAF solving, Apollo-state parsing across four fallback layers, and the keyless sentiment rollup — so you configure a destination or URL list and get typed rows back instead of maintaining a scraping pipeline.

### What's the difference between a Booking.com review scraper and a guest sentiment analysis tool?

A review scraper returns raw text: the reviewer's score, their liked and disliked comments, and metadata like traveller type — one row per review, with no judgment attached. A sentiment analysis tool takes that same review text and turns it into a per-property verdict: how many guests were net positive, what a "recommend rate" looks like, and which specific aspects (staff, cleanliness, location) are driving satisfaction or complaints. The distinction matters because reading fifty raw reviews to decide whether a hotel's cleanliness complaints are trending up is not something most workflows can do at scale, while a single aggregated number or aspect list can be diffed, sorted, and alerted on automatically. Booking.com Scraper: Guest Review Sentiment Analysis returns both in the same run — the raw `review` child rows for anyone who wants the source text, and the `reviewSentiment` object on the parent `hotel` row for anyone who wants the aggregate. You don't have to choose one architecture over the other or run a separate NLP step; both arrive in the same dataset, from the same guest reviews, computed the same way every run.

### How to scrape Booking.com hotels and guest reviews with Booking.com Scraper: Guest Review Sentiment Analysis?

1. Open **Booking.com Scraper: Guest Review Sentiment Analysis** on its Apify Store listing and click **Try for free** (or **Run**, if you already have it saved).
2. Enter a destination in `location` (e.g. `"Paris"`), or paste one or more Booking.com hotel/search-results links into `startUrls` — `location` is ignored whenever `startUrls` is non-empty.
3. Set the query controls that matter for your search: `propertyType`, `starsCountFilter`, `minimumRating`, `sortBy`, `currency`, `language`, and optionally `checkIn` / `checkOut` / `flexWindow` for live-priced results.
4. Leave `scrapeReviews` and `analyzeSentiment` on (both default `true`) and set `maxHotels` / `maxReviewsPerHotel` to your target size, then start the run.
5. Open the **Output** tab to inspect hotel and review rows in the table view, or export the full dataset as JSON, CSV, Excel or XML from the same tab.

```json
{
  "location": "Paris",
  "maxHotels": 10,
  "scrapeReviews": true,
  "maxReviewsPerHotel": 10,
  "analyzeSentiment": true,
  "currency": "EUR",
  "starsCountFilter": "4"
}
```

#### How to run multiple destinations or hotels in one job

There is no separate "batch" input — `startUrls` itself accepts as many Booking.com hotel or search-results links as you want to paste in, and each is scraped in the same run. Note that `maxHotels` is a global ceiling across the whole run, not a per-URL limit: if you paste 50 hotel URLs with `maxHotels` left at its default of 10, only the first 10 hotels (in list order) are scraped and the rest are skipped. For search-results URLs mixed into the same `startUrls` list, each one is expanded into individual hotel links before the same global cap is applied. To cover more than `maxHotels` (20,000 maximum), split the destinations across separate runs — the Actor exposes no built-in concurrency setting to parallelize a single run beyond what one browser context processes sequentially.

### ⬇️ Input

All 25 input fields are optional — there is no required parameter, and an empty run (no `location`, no `startUrls`) exits immediately without scraping anything.

| Parameter | Type | Default | Constraints |
| --- | --- | --- | --- |
| `location` | string | `""` | Destination text (city, region, landmark). Ignored when `startUrls` is non-empty. |
| `startUrls` | array of strings | `[]` | Booking.com hotel detail or search-results page URLs. |
| `maxHotels` | integer | `10` | `minimum: 1`, `maximum: 20000`. Global cap on hotel (parent) rows for the run. |
| `scrapeReviews` | boolean | `true` | Emit per-review child rows. |
| `maxReviewsPerHotel` | integer | `10` | `minimum: 0`, `maximum: 100`. `0` = all reviews Booking embeds on the page. |
| `reviewSortBy` | string (enum) | `"most_relevant"` | `most_relevant` | `newest` | `highest_score` | `lowest_score`. |
| `analyzeSentiment` | boolean | `true` | Adds the keyless `reviewSentiment` object to each hotel row. Free, no key needed. |
| `aiEnhancement` | boolean | `false` | Turns on optional LLM sentiment/topic classification per review. Requires `aiApiKey` (or a matching provider env var) — without one, AI fields stay `null`. |
| `aiModel` | string (enum) | `"claude-haiku-4-5"` | One of 14 models across Anthropic, OpenAI, Google, xAI, DeepSeek, Perplexity, Mistral. Provider is auto-detected from the model name prefix. |
| `aiApiKey` | string, **secret** | — | Provider key matching `aiModel`. Marked `isSecret` — masked in the Apify Console and encrypted at rest. Only read when `aiEnhancement` is `true`. |
| `includeHotelDetails` | boolean | `true` | On: rooms, facilities, images, trader/host info, policies, breadcrumbs. Off: core identity fields only (name, price, rating, address, location, stars). |
| `propertyType` | string (enum) | `"none"` | `none`, `hotels`, `apartments`, `hostels`, `guest houses`, `homestays`, `bed and breakfasts`, `holiday homes`, `boats`, `villas`, `motels`, `resorts`, `holiday parks`, `campsites`, `luxury tents`. |
| `sortBy` | string (enum) | `"distance_from_search"` | `distance_from_search` | `price` | `review_score_and_price` | `review_score` | `class`. |
| `minimumRating` | string | `""` | Minimum guest rating (e.g. `"7"`, `"8"`, `"9"`). Empty = no filter. |
| `starsCountFilter` | string (enum) | `"any"` | `any`, `1`, `2`, `3`, `4`, `5`. |
| `currency` | string (enum) | `"USD"` | `USD`, `EUR`, `GBP`, `CAD`, `AUD`, `CHF`, `JPY`, `CNY`, `INR`, `BRL`, `MXN`. |
| `language` | string (enum) | `"en-gb"` | `en-gb`, `en-us`, `de`, `fr`, `es`, `it`, `pt-br`, `nl`, `pl`, `ru`, `ja`, `zh`. |
| `checkIn` | string | `""` | Absolute `YYYY-MM-DD` or relative (`"2 weeks"`, `"1 month"`). Empty = flexible search. |
| `checkOut` | string | `""` | Same format as `checkIn`. |
| `flexWindow` | string (enum) | `"0"` | `0`, `1`, `2`, `3`, `7` — number of days the stay dates may shift. |
| `rooms` | integer | `1` | `minimum: 1`, `maximum: 9`. |
| `adults` | integer | `2` | `minimum: 1`, `maximum: 30`. |
| `children` | integer | `0` | `minimum: 0`, `maximum: 30`. |
| `minMaxPrice` | string | `"0-999999"` | Free-text range, e.g. `"50-200"` or `"100+"` (minimum only). |
| `proxyConfiguration` | object | `{"useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"]}` | Apify Proxy configuration. Residential is required to clear Booking.com's bot protection. |

```json
{
  "location": "New York",
  "maxHotels": 5,
  "scrapeReviews": true,
  "maxReviewsPerHotel": 10,
  "reviewSortBy": "newest",
  "analyzeSentiment": true,
  "aiEnhancement": false,
  "includeHotelDetails": true,
  "propertyType": "hotels",
  "sortBy": "review_score",
  "starsCountFilter": "4",
  "currency": "USD",
  "language": "en-gb",
  "checkIn": "",
  "checkOut": "",
  "flexWindow": "0",
  "rooms": 1,
  "adults": 2,
  "children": 0,
  "minMaxPrice": "0-999999",
  "proxyConfiguration": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"] }
}
```

**Common pitfall:** `checkIn` / `checkOut` only accept an absolute `YYYY-MM-DD` date or a relative phrase matching `"<number> day(s)/week(s)/month(s)/year(s)"` (e.g. `"2 weeks"`). A free-text value that doesn't match either pattern — `"next week"`, `"tomorrow"` — is silently normalized to an empty string and the search runs without stay dates, so price fields that depend on live pricing come back `null` with no error raised. Similarly, `minMaxPrice` values that don't parse as `"min-max"` or `"min+"` are silently ignored rather than rejected.

### ⬆️ Output

Results are pushed to the Actor's default dataset as typed, normalized JSON — one item per hotel and one item per guest review, interleaved (each hotel's reviews immediately follow it). Export from the Apify Console as JSON, CSV, Excel, XML, RSS or HTML from the same **Export results** menu the platform provides for every dataset.

#### Scraped results

```json
[
  {
    "order": 1,
    "url": "https://www.booking.com/hotel/us/trump-international-new-york.en-gb.html",
    "startUrlOrQuery": "New York",
    "name": "Trump International Hotel & Tower New York",
    "type": "hotel",
    "description": "Overlooking Central Park, this luxury hotel offers spacious suites with kitchens...",
    "stars": 5,
    "price": 780.0,
    "currency": "USD",
    "rating": 9.0,
    "ratingLabel": "Superb",
    "reviews": 794,
    "breakfast": "Available",
    "checkIn": "From 15:00",
    "checkOut": "Until 12:00",
    "checkInDate": null,
    "checkOutDate": null,
    "location": { "lat": 40.7691, "lng": -73.9821 },
    "address": { "full": "One Central Park West", "country": "US", "city": "New York" },
    "image": "https://cf.bstatic.com/xdata/images/hotel/example.jpg",
    "rooms": [
      { "id": "31023", "name": "Deluxe Suite", "description": "City view, kitchenette", "size": "60", "occupancy": 3, "price": 780.0 }
    ],
    "highlights": ["Central Park view", "Spa", "Fitness centre"],
    "finePrint": "Guests are required to show a photo ID and credit card at check-in.",
    "policies": ["Guests are required to show a photo identification and credit card upon check-in.", "The minimum age for check-in is 21."],
    "images": ["https://cf.bstatic.com/xdata/images/hotel/example1.jpg", "https://cf.bstatic.com/xdata/images/hotel/example2.jpg"],
    "roomImages": [{ "roomId": "31023", "images": ["https://cf.bstatic.com/xdata/images/room/example.jpg"] }],
    "categoryReviews": [{ "name": "Staff", "score": 9.4 }, { "name": "Location", "score": 9.8 }],
    "hotelChain": null,
    "licenseInfo": null,
    "hostInfo": null,
    "traderInfo": {
      "isBusiness": true, "email": null, "phone": null, "companyName": "Trump International Hotel LLC",
      "firstName": null, "middleName": null, "lastName": null, "registrationNumber": null, "tradeRegisterName": null,
      "address": { "street": "1 Central Park West", "street2": null, "postalCode": "10023", "city": "New York", "countryCode": "US", "state": "NY" }
    },
    "breadcrumbs": [{ "name": "United States", "url": "https://www.booking.com/country/us.html" }, { "name": "New York", "url": "https://www.booking.com/city/us/new-york.html" }],
    "hotelId": "291324",
    "facilities": [{ "name": "Free WiFi", "id": "107" }, { "name": "Fitness centre", "id": "46" }],
    "timeOfScrapeISO": "2026-07-25T14:02:11.408Z",
    "source_url": "https://www.booking.com/hotel/us/trump-international-new-york.en-gb.html",
    "isChild": false,
    "parentId": null,
    "reviewSentiment": {
      "reviewsAnalyzed": 3, "positiveCount": 2, "neutralCount": 1, "negativeCount": 0,
      "recommendRate": 0.667, "averageReviewScore": 9.0,
      "topAspects": [{ "aspect": "location", "positiveMentions": 2, "negativeMentions": 0, "netSentiment": "positive" }]
    },
    "reviewsCollected": 3
  },
  {
    "type": "review", "isChild": true, "parentId": "291324", "parentHotelName": "Trump International Hotel & Tower New York",
    "hotelUrl": "https://www.booking.com/hotel/us/trump-international-new-york.en-gb.html",
    "reviewId": "5294536437", "reviewerName": "Netta", "reviewerCountry": "GB",
    "travellerType": "Family with young children", "tripPurpose": "Leisure",
    "title": "Perfect stay with kids in New York!",
    "positiveText": "Amazing location right by Central Park, extremely comfortable beds, staff went out of their way to help.",
    "negativeText": null, "reviewScore": 10.0, "sentiment": "positive", "language": "en",
    "roomType": "Deluxe Suite", "reviewedAt": "2026-06-01T16:07:48Z",
    "reviewUrl": "https://www.booking.com/reviewlist.html?r=abc123def456",
    "aiSentiment": null, "aiTopics": null
  },
  {
    "type": "review", "isChild": true, "parentId": "291324", "parentHotelName": "Trump International Hotel & Tower New York",
    "hotelUrl": "https://www.booking.com/hotel/us/trump-international-new-york.en-gb.html",
    "reviewId": "5288120044", "reviewerName": "Marco", "reviewerCountry": "IT",
    "travellerType": "Couple", "tripPurpose": "Leisure",
    "title": "Great location, dated bathroom",
    "positiveText": "Walking distance to everything, very quiet at night.",
    "negativeText": "Bathroom fittings felt a bit dated for the price point.",
    "reviewScore": 7.5, "sentiment": "neutral", "language": "en",
    "roomType": "Executive Room", "reviewedAt": "2026-05-14T09:22:03Z",
    "reviewUrl": "https://www.booking.com/reviewlist.html?r=ghi789jkl012",
    "aiSentiment": null, "aiTopics": null
  },
  {
    "type": "review", "isChild": true, "parentId": "291324", "parentHotelName": "Trump International Hotel & Tower New York",
    "hotelUrl": "https://www.booking.com/hotel/us/trump-international-new-york.en-gb.html",
    "reviewId": "5271044981", "reviewerName": "Chen", "reviewerCountry": "SG",
    "travellerType": "Business traveller", "tripPurpose": "Business",
    "title": "Reliable business stay",
    "positiveText": "Fast WiFi, quiet workspace in the suite, front desk handled a late check-in without any fuss.",
    "negativeText": null, "reviewScore": 9.2, "sentiment": "positive", "language": "en",
    "roomType": "Deluxe Suite", "reviewedAt": "2026-04-29T20:11:57Z",
    "reviewUrl": "https://www.booking.com/reviewlist.html?r=mno345pqr678",
    "aiSentiment": null, "aiTopics": null
  }
]
```

**Hotel row fields (`type: "hotel"`)** — every key `extract_hotel_data()` and the run loop write to the parent row:

| Field | Description |
| --- | --- |
| `order` | 1-based position of this hotel within the run. |
| `url` | Canonical Booking.com hotel URL, from the page's `schema.org` JSON-LD. |
| `startUrlOrQuery` | The `location` search text, or the input URL if one was used. |
| `name` | Hotel name. |
| `type` | Always `"hotel"` on parent rows. |
| `description` | Property description text. |
| `stars` | Official star rating, when present on the page. |
| `price` | Cheapest room price found on the page (see the ⚠️ note below), or `null`. |
| `currency` | Currency code for `price`, e.g. `"USD"`. |
| `rating` | Guest review score (Booking's 1–10 scale). |
| `ratingLabel` | Booking's text label for the score (e.g. `"Superb"`, `"Very good"`). |
| `reviews` | Total review count reported by Booking (not the number of review rows scraped — see `reviewsCollected`). |
| `breakfast` | `"Available"` when breakfast is detected in facilities/highlights or page text, else `null`. |
| `checkIn` / `checkOut` | The hotel's stated check-in/check-out time window (e.g. `"From 15:00"`), not the search dates. |
| `checkInDate` / `checkOutDate` | The stay dates you searched with (from input `checkIn`/`checkOut`), or `null`. |
| `location` | `{ lat, lng }` coordinates. |
| `address` | `{ full, country, city }`. |
| `image` | First property image URL. |
| `rooms` | Array of `{ id, name, description, size, occupancy, price }`, empty when `includeHotelDetails` is off. |
| `highlights` | Array of highlighted amenity strings, empty when `includeHotelDetails` is off. |
| `finePrint` | Concatenated fine-print disclosures, `null` when `includeHotelDetails` is off. |
| `policies` | Array of real house-rule phrases (ID/credit-card requirement, age limits, quiet hours, party/curfew rules), empty array when none are present or when `includeHotelDetails` is off. |
| `images` | Up to 50 image URLs, empty when `includeHotelDetails` is off. |
| `roomImages` | Array of `{ roomId, images[] }`, empty when `includeHotelDetails` is off. |
| `categoryReviews` | Array of `{ name, score }` per-category guest scores (e.g. Staff, Location), empty when `includeHotelDetails` is off. |
| `hotelChain` | Chain name, or `null`. |
| `licenseInfo` | First registered license number, or `null`. |
| `hostInfo` | Host display name for non-corporate listings, or `null`. |
| `traderInfo` | `{ isBusiness, email, phone, companyName, firstName, middleName, lastName, registrationNumber, tradeRegisterName, address }`, `null` when `includeHotelDetails` is off. |
| `breadcrumbs` | Array of `{ name, url }` site breadcrumb links, empty when `includeHotelDetails` is off. |
| `hotelId` | Booking.com's internal property ID — also used as `parentId` on this hotel's review rows. |
| `facilities` | Array of `{ name, id }`, empty when `includeHotelDetails` is off. |
| `timeOfScrapeISO` | UTC timestamp of the scrape. |
| `source_url` | The exact URL fetched for this hotel. |
| `isChild` | Always `false` on hotel rows. |
| `parentId` | Always `null` on hotel rows. |
| `reviewSentiment` | The keyless rollup object described above, or `null` when `analyzeSentiment` is off. |
| `reviewsCollected` | Number of review rows actually collected and pushed for this hotel (after `maxReviewsPerHotel` is applied). |

**Review row fields (`type: "review"`)** — every key `_normalize_featured()` plus the push loop write to each child row:

| Field | Description |
| --- | --- |
| `type` | Always `"review"` on child rows. |
| `isChild` | Always `true` on review rows. |
| `parentId` | The parent hotel's `hotelId`. |
| `parentHotelName` | The parent hotel's `name`, copied for convenience. |
| `hotelUrl` | The parent hotel's `url`. |
| `reviewId` | Booking.com's internal review ID. |
| `reviewerName` | Guest display name (falls back to Booking's own anonymized label when the reviewer opted out). |
| `reviewerCountry` | Guest's country code, upper-cased, or `null`. |
| `travellerType` | Human-readable label (`"Solo traveller"`, `"Couple"`, `"Family with young children"`, `"Family with older children"`, `"Group"`, `"Group of friends"`, `"Business traveller"`, `"With friends"`), or the raw Booking code title-cased if unmapped. |
| `tripPurpose` | `"Leisure"` or `"Business"`, or `null`. |
| `title` | Review headline text, or `null`. |
| `positiveText` | What the guest liked. Booking's placeholder values (`"Nothing"`, `"N/A"`, `"-"`, `"None"`) are normalized to `null`. |
| `negativeText` | What the guest disliked, same placeholder normalization. |
| `reviewScore` | The reviewer's numeric score on Booking's 1–10 scale, or `null`. |
| `sentiment` | Keyless polarity for this single review — `positive` / `neutral` / `negative` / `null` — computed by `review_sentiment()`. |
| `language` | Review language code as tagged by Booking. |
| `roomType` | The room type the reviewer booked, or `null`. |
| `reviewedAt` | Review completion timestamp, ISO 8601 UTC. |
| `reviewUrl` | Deep link to the review on Booking.com, or `null`. |
| `aiSentiment` | LLM-classified sentiment (`positive`/`neutral`/`negative`), populated only when `aiEnhancement` is on with a valid key — otherwise `null`, never faked. |
| `aiTopics` | Up to 4 LLM-generated topic keywords per review, same condition as `aiSentiment`. |

⚠️ **Price note:** `price` is the lowest per-room price the page exposes, taken across the room list — not a nightly-rate-for-your-dates figure unless you supplied `checkIn`/`checkOut`. Without stay dates, Booking often shows no live price at all and `price` comes back `null`.

### How can I use the data extracted with Booking.com Scraper: Guest Review Sentiment Analysis?

- **🏨 Revenue managers and hospitality teams:** pull `reviewSentiment.recommendRate` and `topAspects` for your own property and named competitors to see which specific aspect (staff, cleanliness, breakfast) is dragging the score down before it shows up in the overall rating.
- **🤖 AI engineers and LLM developers:** an agent issues a destination or hotel query, receives the hotel + review + `reviewSentiment` JSON back, and passes it to the model as grounded context — no separate NLP pipeline needed since the sentiment rollup already ships pre-computed.
- **📊 Market researchers:** aggregate `rating`, `price` and `reviewSentiment.averageReviewScore` across every hotel in a destination search to build share-of-voice and price-versus-satisfaction views for a market.
- **🧳 Travel product and OTA teams:** compare `topAspects` net polarity across a shortlist of properties to identify which listings are strong on the aspects your users search for (location, breakfast, Wi-Fi) before they're featured or promoted.

### 📈 How do you monitor guest review sentiment over time?

Guest sentiment is not static — a hotel's `recommendRate` and aspect mix shift as new reviews post, renovations happen, or service quality changes, and Booking.com's own UI gives you no way to see that trend. The pattern is a scheduled run repeated against the same hotel URLs or destination search, with each run's output diffed against the last. The fields worth tracking between runs are `reviewSentiment.recommendRate`, `reviewSentiment.positiveCount` / `.negativeCount`, `reviewSentiment.averageReviewScore`, and the `netSentiment` of specific entries inside `reviewSentiment.topAspects` — a "staff" aspect flipping from `positive` to `mixed` between two runs is a concrete, actionable signal that raw star ratings won't surface for weeks. Because reviews are read from what Booking currently embeds on the page rather than a fixed archive, later runs will naturally include newer reviews and drop older ones out of the ~10-review window, so the rollup itself is already a rolling snapshot rather than a lifetime average — comparing consecutive runs is comparing consecutive rolling windows, which is exactly what you want for catching a recent quality shift. A typical workflow: schedule a weekly run across your tracked hotel URLs → pull `reviewSentiment` from each run's dataset → compare `recommendRate` and `topAspects` against the previous week's export → alert when `recommendRate` drops by a meaningful margin or a previously positive aspect turns negative. Set the recurring run up with Apify's built-in **Schedules** (Console → Schedules → New schedule, pointed at this Actor with a saved input), and either read each run's dataset via the API afterward or wire an Apify **webhook** on the run-succeeded event to notify your own system the moment new data lands.

### Integrate Booking.com Scraper: Guest Review Sentiment Analysis and automate your workflow

Booking.com Scraper: Guest Review Sentiment Analysis works with any language or tool that can call the Apify API — there is no separate signup or authentication scheme beyond your Apify account token.

#### REST API with Python

```python
from apify_client import ApifyClient

client = ApifyClient("<APIFY_API_TOKEN>")

run = client.actor("<YOUR_USERNAME>/booking-scraper-guest-review-sentiment-analysis").call(
    run_input={
        "location": "Lisbon",
        "maxHotels": 10,
        "scrapeReviews": True,
        "maxReviewsPerHotel": 10,
        "analyzeSentiment": True,
    }
)

for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    if item.get("type") == "hotel":
        print(item["name"], item.get("reviewSentiment", {}).get("recommendRate"))
```

Swap `<YOUR_USERNAME>` for your Apify handle if you've made a copy of the Actor under your own account, or call the public Actor ID directly from the Store listing. `<APIFY_API_TOKEN>` is your personal Apify API token from **Console → Settings → Integrations**.

#### Scheduled monitoring and delivery

There is no bespoke webhook system inside the Actor itself — you use the Apify platform's own **Schedules** (recurring runs on a cron expression) and **Webhooks** (HTTP callbacks on run events like `ACTOR.RUN.SUCCEEDED`) to turn a one-off run into an ongoing feed. Both are configured per-Actor from the Apify Console or via the API, and neither requires code changes to this Actor's input.

### Is it legal to scrape Booking.com hotel and review data?

Yes — scraping publicly accessible web pages is generally lawful in most jurisdictions, and Booking.com Scraper: Guest Review Sentiment Analysis only returns what any visitor can already see on a Booking.com hotel or search-results page, without logging in. The bulk of what this Actor returns — hotel names, prices, room descriptions, facilities, addresses — is business/product data, which is governed primarily by Booking.com's own Terms of Service and, in some jurisdictions, database-rights law rather than data-protection law. Guest reviews carry a thinner layer of personal data (a reviewer's display name and country), so if you store or process review data involving EU or California residents at scale, GDPR and CCPA considerations can apply to those specific fields even though the hotel data itself does not implicate them. Scraping for internal monitoring or research carries a different risk profile than scraping to train or fine-tune a model for redistribution — treat the two differently. Consult your legal team for commercial use cases involving bulk data storage or redistribution.

### ❓ Frequently asked questions

#### Does Booking.com Scraper: Guest Review Sentiment Analysis require a Booking.com account or login?

No. Both the hotel scrape and the review scrape read data embedded in the public hotel page's HTML/Apollo state — there is no login step and no session cookie beyond what's needed to clear Booking.com's bot-protection challenge.

#### What languages and currencies does it support?

`language` covers 12 Booking.com locales (`en-gb`, `en-us`, `de`, `fr`, `es`, `it`, `pt-br`, `nl`, `pl`, `ru`, `ja`, `zh`), which changes both the site domain requested and the review-text language returned. `currency` covers 11 currency codes (USD, EUR, GBP, CAD, AUD, CHF, JPY, CNY, INR, BRL, MXN) for displayed prices.

#### How do I control hotel and review order?

`sortBy` controls the order hotels come back in a destination search (distance, price, review score, review score + price, or star class). `reviewSortBy` independently controls each hotel's review order — most relevant (Booking's own order), newest, highest score, or lowest score — before `maxReviewsPerHotel` trims the set.

#### How does Booking.com Scraper: Guest Review Sentiment Analysis handle Booking.com's anti-bot measures?

Booking.com sits behind AWS WAF, which 302-redirects plain HTTP clients before they ever see page content. The Actor drives a headless Playwright Chromium browser through Apify's Residential proxy, waits for the `aws-waf-token` cookie to be issued, and reloads the page until the real content (identified by page size and the presence of the Apollo data block or a property card) is served — retrying with a fresh browser context up to 3 times if the challenge doesn't clear.

#### Does Booking.com Scraper: Guest Review Sentiment Analysis extract a computed sentiment score, not just raw reviews?

Yes. The `reviewSentiment` object on every hotel row is computed deterministically from that hotel's own scraped reviews — polarity from each review's real 1–10 score and liked/disliked text, then aggregated into counts, a recommend rate, an average score and a ranked aspect list. It requires no API key and is on by default (`analyzeSentiment: true`); it is absent (`null`) only when you explicitly turn it off, or trivially empty when `scrapeReviews` is off and there are no reviews to score.

#### How many hotels and reviews does it return per run?

Up to `maxHotels` hotels (1–20,000, default 10) and up to `maxReviewsPerHotel` reviews per hotel (0–100, default 10, where `0` means take everything available). In practice, Booking.com's hotel page embeds only its most recent featured reviews — typically up to around 10 — and this Actor reads that embedded set with no separate pagination request, so setting `maxReviewsPerHotel` above what a given hotel page actually embeds will not produce more rows than the page provides.

#### How do I use it to monitor guest sentiment over time?

Schedule a recurring run over the same hotel URLs or destination with Apify Schedules, then compare `reviewSentiment.recommendRate`, `.positiveCount`/`.negativeCount` and `.topAspects` between consecutive runs' dataset exports, alerting when the recommend rate drops or an aspect's `netSentiment` flips negative.

#### Does Booking.com Scraper: Guest Review Sentiment Analysis work with Claude, ChatGPT and AI agent frameworks?

Yes, as an HTTP endpoint any agent framework can call through the Apify API — there is no dedicated MCP server documented for this Actor. An agent triggers a run with `apify_client` (or a raw REST call), reads the resulting dataset, and grounds its answer in the returned hotel and `reviewSentiment` data.

#### How does Booking.com Scraper: Guest Review Sentiment Analysis compare to other Booking.com review scrapers?

`plowdata/booking-com-review-scraper` also targets Booking.com and returns raw review rows plus Booking's own internal per-category score dataset (location, cleanliness, etc. with confidence intervals) — its listing does not document a computed sentiment or aspect rollup, and it advertises a review-scrape speed of roughly 30–50 reviews/second on its Apify Store page, checked 2026-07-25 — that figure is theirs, not independently measured here. `memo23/hotels-scraper` targets Hotels.com rather than Booking.com and surfaces sentiment tags ("Liked: cleanliness, staff & service") that come from Hotels.com's own GraphQL API rather than being computed by the Actor. Booking.com Scraper: Guest Review Sentiment Analysis is the only one of the three, as checked on the Apify Store 2026-07-25, that returns the full hotel property record, the raw review rows, and a self-computed keyless `reviewSentiment`/`topAspects` rollup together in one dataset.

#### Can I use it without managing proxies or Booking.com credentials?

Yes for proxies — Apify Residential proxy is pre-wired into the default `proxyConfiguration` and is what clears Booking.com's WAF challenge, so no proxy account or IP management is required. No Booking.com credentials are needed at all. The only credential you would ever supply is your own AI provider API key (`aiApiKey`), and only if you turn on the optional `aiEnhancement` classification pass.

### 💬 Your feedback

Found a bug, or a hotel/review field that doesn't match what you see on the live Booking.com page? Open an issue directly from this Actor's page in the Apify Console (the **Issues** tab) with the run ID and the URL that produced the problem — the Scrapier team reviews reports there and ships fixes as new Actor builds.

# Actor input Schema

## `location` (type: `string`):

City, region or landmark to find hotels for (e.g. New York, Paris). Ignored when hotel/search URLs are provided below.

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

One or more Booking.com hotel detail pages or search-results pages. Reviews are read from each hotel page.

## `maxHotels` (type: `integer`):

Maximum number of HOTELS to scrape per destination or start URL. Reviews are counted separately (see maxReviewsPerHotel).

## `scrapeReviews` (type: `boolean`):

Turn on to emit per-review rows (liked/disliked text, score, traveller type, trip purpose, reviewer country, room type, date). Default true.

## `maxReviewsPerHotel` (type: `integer`):

Cap the number of review child rows collected per hotel. Booking embeds up to ~10 recent reviews on each hotel page. Use 0 for all available. Example: maxHotels=5 + maxReviewsPerHotel=10 → up to 5 hotels and up to 50 review rows.

## `reviewSortBy` (type: `string`):

How to order each hotel's reviews before the per-hotel cap is applied.

## `analyzeSentiment` (type: `boolean`):

Adds the `reviewSentiment` object to each hotel row. Keyless and always free. Default true.

## `aiEnhancement` (type: `boolean`):

Optional LLM sentiment + topic tags per review. Requires an API key below. Default false.

## `aiModel` (type: `string`):

Only used when AI is enabled. Provider auto-detected from the name: claude-*=Anthropic, gpt-*/o1/o3=OpenAI, gemini-*=Google, grok-*=xAI, deepseek-*=DeepSeek, sonar*=Perplexity, mistral-\*=Mistral. Cheaper mini/flash/haiku models are recommended for classification.

## `aiApiKey` (type: `string`):

Only used when AI is enabled. Provider key matching the model (Anthropic / OpenAI / Google / xAI / DeepSeek / Perplexity / Mistral). If empty, the actor also checks the matching provider env var (e.g. ANTHROPIC\_API\_KEY, OPENAI\_API\_KEY). Stored securely.

## `includeHotelDetails` (type: `boolean`):

On: rooms, facilities, images, trader/host info, house-rule policies and breadcrumbs. Off: core identity only (name, price, rating, address, location, stars). Default true.

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

Filter search results by type of accommodation.

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

How to sort the hotel search results.

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

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

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

Filter by number of stars.

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

Currency for displayed prices.

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

Interface / review language for Booking.com.

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

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

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

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

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

Allow the 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`):

Residential proxy is strongly recommended (required to pass Booking's bot protection).

## Actor input object example

```json
{
  "location": "New York",
  "startUrls": [
    "https://www.booking.com/hotel/us/trump-international-new-york.en-gb.html"
  ],
  "maxHotels": 5,
  "scrapeReviews": true,
  "maxReviewsPerHotel": 10,
  "reviewSortBy": "most_relevant",
  "analyzeSentiment": true,
  "aiEnhancement": false,
  "aiModel": "claude-haiku-4-5",
  "includeHotelDetails": true,
  "propertyType": "none",
  "sortBy": "review_score",
  "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 = {
    "location": "New York",
    "startUrls": [
        "https://www.booking.com/hotel/us/trump-international-new-york.en-gb.html"
    ],
    "maxHotels": 5,
    "scrapeReviews": true,
    "maxReviewsPerHotel": 10,
    "reviewSortBy": "most_relevant",
    "analyzeSentiment": true,
    "aiEnhancement": false,
    "aiModel": "claude-haiku-4-5",
    "includeHotelDetails": true,
    "propertyType": "none",
    "sortBy": "review_score",
    "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("scrapier/bookingscraper").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 = {
    "location": "New York",
    "startUrls": ["https://www.booking.com/hotel/us/trump-international-new-york.en-gb.html"],
    "maxHotels": 5,
    "scrapeReviews": True,
    "maxReviewsPerHotel": 10,
    "reviewSortBy": "most_relevant",
    "analyzeSentiment": True,
    "aiEnhancement": False,
    "aiModel": "claude-haiku-4-5",
    "includeHotelDetails": True,
    "propertyType": "none",
    "sortBy": "review_score",
    "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("scrapier/bookingscraper").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 '{
  "location": "New York",
  "startUrls": [
    "https://www.booking.com/hotel/us/trump-international-new-york.en-gb.html"
  ],
  "maxHotels": 5,
  "scrapeReviews": true,
  "maxReviewsPerHotel": 10,
  "reviewSortBy": "most_relevant",
  "analyzeSentiment": true,
  "aiEnhancement": false,
  "aiModel": "claude-haiku-4-5",
  "includeHotelDetails": true,
  "propertyType": "none",
  "sortBy": "review_score",
  "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 scrapier/bookingscraper --silent --output-dataset

```

## MCP server setup

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

```

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/qDSaQLd8I9tWouH9A/builds/hstURvpwgp1U7z9Xd/openapi.json
