# Hotel Reviews Scraper (Booking.com + Agoda) (`heyitsgautham/hotel-reviews-scraper`) Actor

Paste Booking.com or Agoda hotel URLs, or hotel names such as The Savoy London, and get guest reviews: score, liked and disliked text, reviewer country, traveler type, stay dates, hotel reply. A name can pull one hotel from both sites. Free summary row. $1.80 per 1,000 reviews, failures free.

- **URL**: https://apify.com/heyitsgautham/hotel-reviews-scraper.md
- **Developed by:** [Gautham Krishna](https://apify.com/heyitsgautham) (community)
- **Categories:** Travel
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.20 / 1,000 reviews

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

## Hotel Reviews Scraper (Booking.com + Agoda)

Get guest reviews from Booking.com and Agoda in one actor, for hotel managers, travel researchers and analysts comparing properties.

Use cases, each with a ready-to-run example:

- [Get the Booking.com reviews of a hotel](https://apify.com/heyitsgautham/hotel-reviews-scraper/examples/booking-com-reviews-the-savoy)
- [Find a hotel by its name, with no URL needed](https://apify.com/heyitsgautham/hotel-reviews-scraper/examples/booking-reviews-by-hotel-name)
- [Get Agoda reviews for a list of Bangkok hotels](https://apify.com/heyitsgautham/hotel-reviews-scraper/examples/agoda-reviews-bangkok-hotels)
- [Read a hotel's lowest-rated reviews first](https://apify.com/heyitsgautham/hotel-reviews-scraper/examples/lowest-rated-hotel-reviews)
- [Get Booking.com and Agoda reviews of the same hotel together](https://apify.com/heyitsgautham/hotel-reviews-scraper/examples/booking-and-agoda-reviews-same-hotel)
- [Keep only the reviews written in one language](https://apify.com/heyitsgautham/hotel-reviews-scraper/examples/hotel-reviews-in-german)

Paste hotel URLs from either site and the actor detects which one it is, or type hotel names such as "The Savoy London".
Each review comes back as one clean row with the score, the liked and disliked text, the reviewer's country, traveler type, room type, stay dates, review date, language and the hotel's reply.
A name can be looked up on both sites in one run, and the actor scrapes both only when the two listings are the same hotel.
A free summary row per hotel gives the review count, average score, score bands, date range and the split per site.

You pay only for reviews returned. Hotels that fail or have no reviews cost nothing.

### What you get

One dataset item per review.

| Field | What it is |
|---|---|
| `site` | `booking` or `agoda`, detected from the URL, or the site you chose for names |
| `hotelName`, `hotelId`, `hotelUrl` | The hotel the review belongs to |
| `reviewSource` | Who collected the review. Agoda pages also show Booking.com reviews, so Agoda rows say `Agoda` or `Booking.com` |
| `reviewId` | The site's own id for the review |
| `rating`, `ratingScale` | Guest score. Both sites use a 10-point scale |
| `title` | Review headline |
| `positive`, `negative` | What the guest liked and disliked, when the site splits them |
| `text` | The full review text. For split reviews it is `Liked: ...` and `Disliked: ...` joined |
| `reviewerName` | The public display name shown on the site, or `null` for anonymous reviews |
| `reviewerCountry`, `reviewerCountryCode` | Where the guest is from |
| `travelerType` | Couple, Family, Solo traveler, Group, Business traveler |
| `roomType`, `nights`, `checkInDate`, `checkOutDate` | The stay being reviewed |
| `reviewDate` | When the review was posted, in UTC |
| `language` | Language of the review text, such as `en`, `de`, `zh-CN`. `null` when the guest left only a score |
| `helpfulVotes` | Helpful votes, when the site shows them |
| `hotelResponse`, `hotelResponseDate` | The property's public reply. Booking.com does not show a reply date |
| `input` | The URL or name you passed |
| `matchedHotel` | Only on rows from a hotel name. The hotel the name matched, with its location as the site shows it |

With `hotelSummary` on, each hotel also gets one free row with `status: "summary"`, described below.

The actor never collects emails, phone numbers or profile links.

### Input

```json
{
  "hotelUrls": [
    "https://www.booking.com/hotel/gb/the-savoy.html",
    "https://www.agoda.com/the-peninsula-hong-kong/hotel/hong-kong-hk.html"
  ],
  "maxReviewsPerHotel": 100,
  "sortBy": "newest",
  "languages": ["en", "de"]
}
```

| Field | Default | Notes |
|---|---|---|
| `hotelUrls` | empty | Booking.com `/hotel/<country>/<name>.html` links in any language version, and Agoda hotel or review page links. Agoda links with a `hotel_id` or `hid` parameter also work |
| `hotelNames` | empty | Hotel names with the city, such as `The Savoy London`. See Hotel names below. Pass URLs, names or both |
| `nameSite` | `booking` | Which site to search the names on, `booking`, `agoda` or `both`. See Both sites from one name below |
| `maxReviewsPerHotel` | 100 | Stops after this many reviews per hotel |
| `sortBy` | `newest` | `newest`, `relevant`, `highest` or `lowest`. On Agoda, `relevant` uses Agoda's "Most helpful" order |
| `languages` | all | ISO codes such as `en`, `fr`, `ja`, `zh`. The site filters before sending, so you do not pay for reviews you do not want |
| `hotelSummary` | `false` | Adds one free summary row per hotel. See Free summary row below |
| `onlyNewSinceLastRun` | `false` | See Monitoring below |
| `publishedAfter` | empty | See Monitoring below |
| `maxConcurrency` | 5 | Hotels scraped in parallel |

### Monitoring: only new reviews and a date cutoff

Two optional inputs turn a scheduled run into a feed of new reviews.
Both are off by default, and a run without them works exactly as before.

| Option | What it does |
|---|---|
| `onlyNewSinceLastRun` | Returns only reviews this scraper has not returned to you before for the same hotel and filters, and nothing older than the newest one it returned last time. The first run returns the usual results and remembers them |
| `publishedAfter` | Only reviews published on or after a date: `2026-10-01`, `2026-10-01T12:00:00Z` (UTC), or a relative value such as `7 days` |

With `sortBy: "newest"` the run stops at the first page that holds nothing new, so a daily run reads only the pages it needs.
With other orders it still skips old reviews, but it reads further to find new ones.
You pay only for the reviews returned.
The memory is a key-value store named `hotel-reviews-scraper-monitor` in your own Apify account, one record per hotel and language filter.
Delete that store to start over.
A hotel with nothing new comes back as a free `no_reviews` row.

### Run it on a schedule

A weekly run with `onlyNewSinceLastRun: true` and `sortBy: "newest"` gives you the new guest reviews for the hotels you track.
We ran the same 69 Booking.com and Agoda hotels on 15 Oct and again on 20 Oct.
The 20 Oct run returned 28 reviews, none returned on 15 Oct, and 57 hotels with nothing new came back as free `no_reviews` rows.

1. Set your input with `onlyNewSinceLastRun: true` and click **Save as a new task**.
2. On the task page, click **Actions**, then **Schedule**, and pick how often it runs.
   [Apify's schedules guide](https://docs.apify.com/actors/running/schedules) covers cron times and time zones.
3. On the task's **Integrations** tab, choose where each run's results go.

| Integration | What it does |
|---|---|
| [Slack](https://docs.apify.com/integrations/slack) | Posts a message to your channel when a run finishes |
| [Google Drive](https://docs.apify.com/integrations/drive) | Saves each run's results as a file in your Drive |
| [Webhook](https://docs.apify.com/integrations/webhooks) | Sends the finished run, with its dataset ID, to your own URL |
| [Make](https://docs.apify.com/integrations/make), [Zapier](https://docs.apify.com/integrations/zapier), [n8n](https://docs.apify.com/integrations/n8n) | Adds each new row to a Google Sheet, a CRM or a database |

Each review has a stable `reviewId`, so a sheet or database can skip rows it already holds.

### Hotel names instead of URLs

Put names in `hotelNames` and pick the site in `nameSite`.

```json
{
  "hotelNames": ["The Savoy London", "Mandarin Oriental Bangkok", "Hotel Arts Barcelona"],
  "nameSite": "booking",
  "maxReviewsPerHotel": 100
}
```

The actor types each name into the site's own search box, the way you would, and reads the hotels it suggests.
It then scrapes the one hotel the name matches.
Every review row from a name carries `matchedHotel`, so you can see which hotel you got:

```json
{
  "status": "ok",
  "site": "booking",
  "hotelId": "280149",
  "hotelName": "The Savoy",
  "hotelUrl": "https://www.booking.com/hotel/gb/the-savoy.html",
  "reviewSource": "Booking.com",
  "reviewId": "2e0f2f561783aa35",
  "rating": 9,
  "ratingScale": 10,
  "title": null,
  "positive": "All the staff behaved in a professional  manner, always put the customer  first",
  "negative": "The late timing of  cleaning the suites, staff would turn up at about  5pm, to clean the room. Then the turn down  could happen  any time after 5pm which seemed strange.",
  "text": "Liked: All the staff behaved in a professional  manner, always put the customer  first\nDisliked: The late timing of  cleaning the suites, staff would turn up at about  5pm, to clean the room. Then the turn down  could happen  any time after 5pm which seemed strange.",
  "reviewerName": "Ann",
  "reviewerCountry": "United Kingdom",
  "reviewerCountryCode": "GB",
  "travelerType": "Solo traveler",
  "roomType": "Superior Queen Room",
  "nights": 3,
  "checkInDate": "2026-10-07",
  "checkOutDate": "2026-10-10",
  "reviewDate": "2026-10-10T21:04:53Z",
  "language": "en",
  "helpfulVotes": 0,
  "hotelResponse": "Dear Ann, Thank you for staying with us at The Savoy and for taking the time to complete a guest satisfaction survey. We are so happy to hear you had a great Savoy experience and enjoyed the personal touches and service from the team.  We do value guest feedback and I will share your additional comments with our housekeeping team, I do hope this did not interrupt your stay at any point. Thank you again for sharing your thoughts and we very much look forward to warmly welcoming you back to The Savoy in the future. With best regards, Debra Patterson, Quality Leader",
  "hotelResponseDate": null,
  "input": "The Savoy London",
  "matchedHotel": "The Savoy, London, Greater London, United Kingdom"
}
```

How a name is matched:

- Every word you typed must be in the hotel's name or its location.
- The hotel's name must start with a word you typed.
- The hotel's name must not hold a word such as "near" or "view" that you did not type, which skips rentals named after a landmark such as "Peaceful 2BR Retreat Near Burj Al Arab".
- Words such as "the", "hotel" and "hostel" are ignored, and accents do not matter.
- When several hotels match, the ones whose location holds the most of your words stay.
- A hotel is an exact match when every word of its name is one you typed or part of its own location.
- The actor scrapes the only match left, or else the first exact match.
- Two listings with the same name in the same place, such as "The Savoy" and "The Savoy Hotel" in London on Agoda, count as one hotel, and the site's first listing wins.

A name is ambiguous, and nothing is scraped, when two or more hotels still match and none is a clear exact match.
That covers chains typed without the branch ("Hilton London"), two real hotels with the same name ("Wombat's City Hostel Munich" has two branches in Munich), and an exact match listed below a longer name that starts with your words.
You get one free row listing up to 5 candidates with their URLs, so you can pass the right URL in `hotelUrls` on the next run:

```json
{"input": "Wombat's City Hostel Munich", "status": "ambiguous", "site": "booking", "message": "2 Booking.com hotels match this name equally well, so none was scraped. Pass the URL of the one you want in hotelUrls, or add more of its name", "candidates": [{"hotelName": "Wombat's City Hostel Munich Hauptbahnhof", "location": "Munich, Bavaria, Germany", "hotelId": "180960", "hotelUrl": "https://www.booking.com/hotel/de/wombats-hostel-munich.html"}, {"hotelName": "Wombat's City Hostel Munich Werksviertel", "location": "Munich, Bavaria, Germany", "hotelId": "7049815", "hotelUrl": "https://www.booking.com/hotel/de/wombats-the-city-hostel-munich-werksviertel.html"}]}
```

A name that matches no hotel also gives one free row:

```json
{"input": "Hotel d'Angleterre Copenhagen", "status": "not_found", "site": "booking", "message": "No Booking.com hotel matches this name. Check the spelling, add the city, or pass the hotel's URL in hotelUrls"}
```

Searching a name is never charged.
A URL is always the most precise input.

### Both sites from one name

Set `nameSite` to `both` and each name is looked up on Booking.com and on Agoda.

```json
{
  "hotelNames": ["The Savoy London", "Mandarin Oriental Bangkok", "Hotel Arts Barcelona"],
  "nameSite": "both",
  "hotelSummary": true,
  "maxReviewsPerHotel": 100
}
```

Each site first picks its hotel by the name rules above.
The actor then checks that the two listings are one hotel.

- The names must agree. Both have the same words once the location is set aside, or one name holds all the words of the other ("Hotel Emma" and "Hotel Emma at Pearl on the Riverwalk").
- An extra word that marks a different listing breaks the match, such as "Apartments", "Suite", "2BR" or a branch number.
- Both listings must be in the same country.
- Their map pins must be within 1 km when the names have the same words, and within 250 m when one name only holds the other.

When the two picks fail the check, or one site has no clear pick, the actor looks among the other site's results for that name for a listing that passes the check against the site that did pick.
That finds a hotel listed under a newer name, such as "Eco Boutique Hostal Grau" on Booking.com for "Hostal Grau Barcelona".
When Agoda lists the paired hotel twice, the actor takes the listing with the most Agoda reviews.

Nothing unsure is paired.
When only one site has a clear match, that site is scraped and the other gets a free row saying why:

```json
{"input": "Hotel Sacher Wien", "status": "not_found", "site": "agoda", "message": "No Agoda hotel matches this name at the location of the Booking.com match, Hotel Sacher Wien, Vienna, Vienna (state), Austria, so only Booking.com was scraped"}
```

When each site picks a different hotel, neither is scraped, because one of them is wrong and nothing says which.
Each site gets a free `mismatch` row with its pick and URL:

```json
{"input": "The Siam Hotel Bangkok", "status": "mismatch", "site": "agoda", "message": "Agoda's match, The Siam Hotel, Bangkok, Thailand, and Booking.com's match, Bangkok Siam, Bangkok, Bangkok Province, Thailand, are not the same hotel by name and location. They are 7.6 km apart. Neither was scraped. Pass the URL of the hotel you want in hotelUrls, or add more of its name", "candidates": [{"hotelName": "The Siam Hotel", "location": "Bangkok, Thailand", "hotelId": "296765", "hotelUrl": "https://www.agoda.com/partners/partnersearch.aspx?hid=296765"}]}
```

Agoda pages also show Booking.com reviews.
For a paired hotel those are the same reviews the Booking.com rows already hold, so the Agoda rows keep only Agoda's own reviews and you never pay twice for one review.
In our check on The Savoy, 780 of the 789 Booking.com reviews with text that Agoda showed were word for word on Booking.com.
A hotel found on Agoda alone keeps both kinds, as before.

`maxReviewsPerHotel` applies on each site, so `10` gives up to 10 Booking.com reviews and up to 10 Agoda reviews.
Every row says its `site`, and each site's rows carry that site's `matchedHotel`.

We tested 76 real hotel names with `both` on 17 October 2026 (run `IeiOtf8dAMnbcczKR`), covering luxury hotels, resorts, chains and hostels in Europe, Asia, the Middle East, the Americas, Africa and Australia, and checked every pair by hand.

| Outcome | Names |
|---|---|
| Same hotel found and scraped on both sites | 64 |
| Paired with the wrong hotel | 0 |
| Scraped on one site only, because the other site's search box did not suggest the hotel | 6 |
| Ambiguous on both sites, free rows with candidates | 5 |
| Each site picked a different hotel, free `mismatch` rows | 1 |

The 64 pairs had their two map pins a median of 20 m apart, and at most 497 m for a large resort (The Ritz-Carlton Bali).
All 591 Agoda reviews of the paired hotels were Agoda's own.
The run took 5.5 minutes for 76 names at the default concurrency of 5, well inside the default 1-hour run timeout.
One of the six one-site hotels, Hotel d'Angleterre Copenhagen, had no reviews on Agoda and is not in Booking.com's search.

### Free summary row

Set `hotelSummary` to `true` to get one more row per hotel, after its reviews.
It is never charged and needs no extra request, because it is worked out from the reviews already returned and the figures each site sends with them.
Hotels that return no review get no summary row.

| Field | What it is |
|---|---|
| `site` | `booking`, `agoda`, or `both` for a hotel matched on both sites |
| `hotelName` | The hotel's name, Booking.com's when both sites were scraped |
| `matchDistanceMeters` | Only with `both`. How far apart the two sites put the hotel's map pin |
| `returnedReviews` | Figures for the reviews this run returned, all sites together: `count`, `averageRating`, `oldestDate`, `newestDate`, `ratingDistribution` in Agoda's score bands (`9+`, `8-9`, `7-8`, `6-7`, `<6`), `lowRatingPercent` (below 6 of 10), `hotelReplyPercent`, and counts of `travelerTypes`, `reviewerCountries` (top 10), `languages` and `reviewSources` |
| `bySite` | One block per site with the hotel's ID, name, URL and `matchedHotel`, the site's own `siteRating`, `siteReviewCount` and `siteSubRatings` for all of the hotel's reviews, Agoda's `siteRatingDistribution`, and the `count`, average and date range of the reviews returned from that site |

On Agoda, `siteRating` and `siteReviewCount` cover Agoda's own guest reviews.
Booking.com shows no score bands, so its `siteRatingDistribution` is `null`.
With `onlyNewSinceLastRun`, `returnedReviews` covers only the new reviews.

A real summary row from the test run:

```json
{
  "status": "summary",
  "input": "The Savoy London",
  "site": "both",
  "hotelName": "The Savoy",
  "matchDistanceMeters": 133,
  "returnedReviews": {
    "count": 20,
    "averageRating": 9.1,
    "oldestDate": "2024-05-25T03:07:00Z",
    "newestDate": "2026-10-15T12:32:16Z",
    "ratingDistribution": {
      "9+": 16,
      "8-9": 2,
      "7-8": 0,
      "6-7": 0,
      "<6": 2
    },
    "lowRatingPercent": 10,
    "hotelReplyPercent": 70,
    "travelerTypes": {
      "Couple": 12,
      "Solo traveler": 6,
      "Family with young children": 1,
      "Group": 1
    },
    "reviewerCountries": {
      "United Kingdom": 8,
      "United States": 3,
      "Belgium": 2,
      "Japan": 2,
      "Australia": 1,
      "Canada": 1,
      "Ireland": 1,
      "Russia": 1,
      "South Korea": 1
    },
    "languages": {
      "en": 12,
      "ja": 2,
      "nl": 2,
      "ko": 1,
      "ru": 1
    },
    "reviewSources": {
      "Agoda": 10,
      "Booking.com": 10
    }
  },
  "bySite": {
    "booking": {
      "hotelId": "280149",
      "hotelName": "The Savoy",
      "hotelUrl": "https://www.booking.com/hotel/gb/the-savoy.html",
      "matchedHotel": "The Savoy, London, Greater London, United Kingdom",
      "siteRating": 9.4,
      "siteReviewCount": 1433,
      "siteRatingDistribution": null,
      "siteSubRatings": {
        "Location": 9.7,
        "Cleanliness": 9.7,
        "Value for money": 8.5,
        "Facilities": 9.5,
        "Comfort": 9.8,
        "Staff": 9.7,
        "Free Wifi": 9.5
      },
      "returnedReviews": {
        "count": 10,
        "averageRating": 8.6,
        "oldestDate": "2026-10-07T08:01:27Z",
        "newestDate": "2026-10-15T12:32:16Z"
      }
    },
    "agoda": {
      "hotelId": "11070",
      "hotelName": "The Savoy Hotel",
      "hotelUrl": "https://www.agoda.com/the-savoy-hotel/hotel/london-gb.html",
      "matchedHotel": "The Savoy Hotel, London, United Kingdom",
      "siteRating": 9.2,
      "siteReviewCount": 210,
      "siteRatingDistribution": {
        "9+": 144,
        "8-9": 25,
        "7-8": 9,
        "6-7": 5,
        "<6": 4
      },
      "siteSubRatings": {
        "Cleanliness": 9.6,
        "Facilities": 9.2,
        "Location": 9.6,
        "Room comfort and quality": 9.3,
        "Service": 9.5,
        "Value for money": 8.4
      },
      "returnedReviews": {
        "count": 10,
        "averageRating": 9.6,
        "oldestDate": "2024-05-25T03:07:00Z",
        "newestDate": "2026-08-18T05:09:56Z"
      }
    }
  },
  "message": "Free summary. returnedReviews covers the 20 reviews this run returned. siteRating and siteReviewCount in bySite are each site's own figures for all of the hotel's reviews."
}
```

### Each hotel once per run

When two inputs in one run reach the same hotel, the first one in your list scrapes it and each later one gets a free `duplicate` row.
This covers a name and a URL for one hotel, a URL pasted twice, and two URL variants such as `the-savoy.html` and `the-savoy.en-gb.html`.
The actor compares the site's own hotel ID, so you never pay twice for the same reviews.
For this, every entry in `hotelUrls` counts as earlier in the list than any entry in `hotelNames`.

```json
{"input": "The Savoy London", "status": "duplicate", "site": "booking", "hotelUrl": "https://www.booking.com/hotel/gb/the-savoy.html", "matchedHotel": "The Savoy, London, Greater London, United Kingdom", "message": "Same Booking.com hotel as the input https://www.booking.com/hotel/gb/the-savoy.html, so its reviews are listed once", "hotelId": "280149", "duplicateOf": "https://www.booking.com/hotel/gb/the-savoy.html"}
```

We tested 60 real hotel names on Booking.com (run `m64YscdPzyfbXwzDF`) and 50 on Agoda (run `w0cRzFMU4LtVNgzlP`) on 16 October 2026.
The names cover luxury hotels, chains, boutique hotels and hostels in Europe, Asia, the Americas, Africa and Australia.

| | Booking.com | Agoda |
|---|---|---|
| Names tried | 60 | 50 |
| Matched the right hotel and returned reviews | 52 | 45 |
| Matched a wrong hotel | 0 | 0 |
| Ambiguous, free row with candidates | 3 | 2 |
| Not found, free row | 5 | 3 |

Of the 5 Booking.com names not found, 2 never appear in Booking.com's own search box, Clink78 Hostel London and Hotel d'Angleterre Copenhagen.
The other 3 are on Booking.com under another name.
"Hotel Ritz Madrid" is now "Mandarin Oriental Ritz, Madrid", "Hostal Grau Barcelona" is "Eco Boutique Hostal Grau", and "Hôtel des Grands Boulevards Paris" is "Grands Boulevards Experimental".
The actor does not guess at renamed hotels, so type the name the site shows now, or pass the URL.
The 3 Agoda names not found were "Ibis Styles Bangkok Khaosan Viengtai", "Hotel G Singapore" and "The Manila Hotel", which Agoda's search box did not suggest for those words.

### Output example

Real rows from a test run on 2026-10-10.

```json
[
  {
    "status": "ok",
    "site": "booking",
    "hotelId": "14518094",
    "hotelName": "Schlaffässer Ferienhaus Berger",
    "hotelUrl": "https://www.booking.com/hotel/de/ferienhaus-berger-bad-karlshafen.html",
    "reviewSource": "Booking.com",
    "reviewId": "a52123065d1efb62",
    "rating": 10,
    "ratingScale": 10,
    "title": "Ein tolles Erlebnis",
    "positive": "Einfach alles spitze, die Vermieter super nett. Es ist alles vorhanden was man so braucht und es ist alles sehr sauber. Wir haben uns sehr wohl gefühlt.",
    "negative": "Da gibt es nichts.",
    "text": "Liked: Einfach alles spitze, die Vermieter super nett. Es ist alles vorhanden was man so braucht und es ist alles sehr sauber. Wir haben uns sehr wohl gefühlt.\nDisliked: Da gibt es nichts.",
    "reviewerName": "Nadine",
    "reviewerCountry": "Germany",
    "reviewerCountryCode": "DE",
    "travelerType": "Family",
    "roomType": "Double Room with Garden View",
    "nights": 2,
    "checkInDate": "2026-08-28",
    "checkOutDate": "2026-08-30",
    "reviewDate": "2026-08-31T17:34:40Z",
    "language": "de",
    "helpfulVotes": 0,
    "hotelResponse": "Vielen Dank für die tolle Bewertung! 😊 Es freut uns sehr, dass Sie sich bei uns wohlgefühlt haben. Bis zum nächsten Mal!",
    "hotelResponseDate": null,
    "input": "https://www.booking.com/hotel/de/ferienhaus-berger-bad-karlshafen.html"
  },
  {
    "status": "ok",
    "site": "agoda",
    "hotelId": "344324",
    "hotelName": "Residence & U",
    "hotelUrl": "https://www.agoda.com/residence-u/hotel/goyang-si-kr.html",
    "reviewSource": "Agoda",
    "reviewId": "225149880",
    "rating": 8.4,
    "ratingScale": 10,
    "title": "Very nice place",
    "positive": null,
    "negative": null,
    "text": "This residence&u Near shop, restaurants, park, department store.\nThe room are very convenient and very clean.\nGm team and all staff very kindness and helpful.",
    "reviewerName": "AKSORN",
    "reviewerCountry": "Thailand",
    "reviewerCountryCode": "TH",
    "travelerType": "Business traveler",
    "roomType": "Studio Double",
    "nights": 5,
    "checkInDate": "2019-06-12",
    "checkOutDate": "2019-06-17",
    "reviewDate": "2019-06-22T02:42:00Z",
    "language": "en",
    "helpfulVotes": 0,
    "hotelResponse": "감사합니다.\n\n매번 킨텍스 행사를 통해서 오시는 손님들을 저희는 환영합니다.\n\n다음 번에 또 행사가 있으시면 또 방문 해 주시기 바랍니다.\n\n또 뵙기를 희망 합니다.",
    "hotelResponseDate": "2019-07-03T01:50:45Z",
    "input": "https://www.agoda.com/residence-u/hotel/goyang-si-kr.html"
  }
]
```

When a hotel gives no reviews, you get one free row instead, for example:

```json
{"input": "https://www.booking.com/hotel/no/luxury-arctic-cabin.html", "status": "no_reviews", "site": "booking", "hotelUrl": "https://www.booking.com/hotel/no/luxury-arctic-cabin.html", "message": "This hotel has no reviews"}
```

Other statuses are `not_found` (the URL or name points to no hotel), `ambiguous` (a name matches several hotels), `mismatch` (with `both`, the two sites picked different hotels), `duplicate` (an earlier input in the run already covers this hotel), `invalid_url`, `invalid_input`, `error` and `partial` (stopped part way, the reviews before the stop are kept).
With `hotelSummary` on, the summary rows have `status: "summary"`.

### Pricing

Pay per event. One `review` event per review in your dataset.

| Reviews | No discount | Bronze | Silver | Gold |
|---|---|---|---|---|
| 1,000 | $1.80 | $1.60 | $1.40 | $1.20 |
| 10,000 | $18.00 | $16.00 | $14.00 | $12.00 |
| 100,000 | $180.00 | $160.00 | $140.00 | $120.00 |

Bronze, Silver and Gold are Apify Store discount tiers, and Apify's Pricing tab shows them as Starter, Scale and Business.
Apify decides which tier an account is on.

Status rows for failed, empty or duplicate hotels, summary rows, and searching hotel names, are never charged.
If you set a maximum charge for a run, the actor stops cleanly when it is reached.

### How it works

The actor reads the same public JSON that the hotel pages load in a logged-out browser.
It does not render pages in a browser, so runs are fast and cheap.
Booking.com returns 25 reviews per request and Agoda 50.
Requests go through Apify residential proxies with a fresh IP on every block or empty answer.

In our test of 69 real hotels by URL (38 Booking.com, 31 Agoda, big city hotels and single holiday homes, run `bKFygJYdKAjnJbiJE` on 16 October 2026), 59 returned reviews.
The 10 that returned nothing had no reviews in the site's review data.

### More from heyitsgautham

Other actors from the same account.

- [Tripadvisor Reviews Scraper: Hotels, Restaurants, Tours](https://apify.com/heyitsgautham/tripadvisor-reviews-scraper) for Tripadvisor reviews of hotels, restaurants, attractions and tours, with sub-ratings and owner responses.
- [Trustpilot Reviews & TrustScore Scraper](https://apify.com/heyitsgautham/trustpilot-reviews-scraper) for Trustpilot reviews and each company's TrustScore, for any business on any Trustpilot country site.
- [Capterra Reviews Scraper](https://apify.com/heyitsgautham/software-reviews-scraper) for Capterra software reviews, by product URL or product name.
- [YouTube Video & Shorts Comments Scraper with Replies](https://apify.com/heyitsgautham/youtube-comments-scraper) for the comments and replies on a video or Short, for sentiment analysis and audience research.
- [Google Trends Scraper: Related Queries & Trending Searches](https://apify.com/heyitsgautham/google-trends-scraper) for Google Trends interest over time, related queries and Trending Now.

### FAQ

**Do I need a Booking.com or Agoda account?**
No. The actor only reads what anyone can see without logging in.

**Can I get all reviews of a hotel?**
Yes. Set `maxReviewsPerHotel` above the hotel's review count. Booking.com hotels with thousands of reviews page through 25 at a time.

**Why do Agoda results include Booking.com reviews?**
Agoda shows reviews from its sister site on its own pages. The `reviewSource` field tells them apart, so you can filter them out.

**Are reviews translated?**
No. You get the text in the language the guest wrote it. Use `languages` to keep only the ones you want.

**Can I give a hotel name instead of a URL?**
Yes. Put it in `hotelNames` with the city, such as `The Savoy London`, and choose the site in `nameSite`. See Hotel names above.

**Can I get one hotel's reviews from both Booking.com and Agoda?**
Yes. Set `nameSite` to `both`. The actor scrapes both sites only when the two listings are the same hotel by name, country and map pin. See Both sites from one name above.

**Can I get the average score and score breakdown without reading every review?**
Yes. Set `hotelSummary` to `true` for one free summary row per hotel with the average, score bands, date range and each site's own rating and review count.

**Can I search by city instead of hotel URL?**
Not in this actor. It takes hotel page URLs or hotel names, one hotel each.

### Limitations

- Only Booking.com and Agoda. Other domains return `invalid_url`.
- Hotel names are searched on the site in `nameSite`, or on both with `both`. URLs always use their own site, and the actor does not look a URL's hotel up on the other site.
- A name finds a hotel only when the site's search box suggests it. A hotel listed under a newer name needs that name or its URL.
- `newest` order on Agoda follows Agoda's own list. Agoda keeps its own reviews and the Booking.com reviews it shows in separate blocks, so the two are not merged by date across pages.
- Agoda language filtering supports these codes: en, fr, de, it, es, ja, zh, zh-cn, zh-tw, zh-hk, ko, ru, pt, pt-br, nl, th, ms, vi, sv, id, pl, no, cs, tr, ca, hu, he, ar, lv.
- Booking.com does not publish a date for the hotel's reply.
- Agoda sometimes answers with an empty or partial list. The actor checks several IPs and keeps the fullest answer, which adds a few seconds per Agoda hotel.
- Both sites can change their pages at any time. If something breaks, open an issue and it will be fixed.

# Actor input Schema

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

Booking.com or Agoda hotel page URLs, or leave empty and use Hotel names below. The site is detected from the URL. Booking.com: https://www.booking.com/hotel/gb/the-savoy.html (any language suffix or query string is fine). Agoda: https://www.agoda.com/the-peninsula-hong-kong/hotel/hong-kong-hk.html, or any Agoda link with a hotel_id or hid parameter.

## `hotelNames` (type: `array`):

Hotel names as you would type them into the site's search box, with the city, such as The Savoy London or Mandarin Oriental Bangkok. The actor searches the site below and scrapes the hotel the name matches, and every row says which hotel that was in matchedHotel. A name that matches no hotel, or several hotels equally well (Hilton London), gives a free row instead, and the tie row lists up to 5 candidates with their URLs. A URL is always the most precise input.

## `nameSite` (type: `string`):

Which site's reviews to get for the hotel names above. URLs pick their site themselves. Both looks each name up on Booking.com and Agoda and scrapes both only when the two listings are the same hotel by name, country and map position. A paired hotel's Agoda rows then hold only Agoda's own reviews, since the Booking.com reviews Agoda shows are already in the Booking.com rows. When only one site has a clear match, that site is scraped alone and the other gets a free row saying why. The review limit applies on each site.

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

Stops after this many reviews for each hotel. You pay only for reviews returned.

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

Which reviews to take first. On Agoda, Most relevant uses Agoda's Most helpful order.

## `languages` (type: `array`):

Only return reviews written in these languages, as ISO 639-1 codes such as en, de, fr, ja, zh. Leave empty for all languages.

## `hotelSummary` (type: `boolean`):

Adds one free row with status summary per hotel, after its reviews. It gives the reviews this run returned (count, average score, score bands, date range, low-score share, hotel reply share, traveler types, reviewer countries, languages and review sources) and each site's own rating, review count and sub-ratings for the whole hotel, with Agoda's score bands. With both sites it splits the figures per site. It is never charged and needs no extra request.

## `onlyNewSinceLastRun` (type: `boolean`):

For scheduled runs. Returns only reviews this actor has not returned to you before for the same hotel and filters, and nothing older than the newest one it returned last time. The first run returns the usual results and remembers them. Works best with newest-first order, where the run stops at the first page with nothing new. The memory is a key-value store named hotel-reviews-scraper-monitor in your Apify account.

## `publishedAfter` (type: `string`):

Only reviews published on or after this date. Use a date such as 2026-10-01, an ISO time such as 2026-10-01T12:00:00Z (UTC), or a relative value such as 7 days, meaning the last 7 days. With newest-first order the run stops as soon as it reaches older ones.

## `maxConcurrency` (type: `integer`):

How many hotels to scrape in parallel.

## `useProxy` (type: `boolean`):

Leave on. Both sites block or empty out answers to cloud IPs.

## Actor input object example

```json
{
  "hotelUrls": [
    "https://www.booking.com/hotel/gb/the-savoy.html",
    "https://www.agoda.com/the-peninsula-hong-kong/hotel/hong-kong-hk.html"
  ],
  "hotelNames": [],
  "nameSite": "booking",
  "maxReviewsPerHotel": 20,
  "sortBy": "newest",
  "languages": [],
  "hotelSummary": false,
  "onlyNewSinceLastRun": false,
  "maxConcurrency": 5,
  "useProxy": true
}
```

# Actor output Schema

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

One item per review with site, hotel, rating, title, liked and disliked text, reviewer country, traveler type, room type, stay dates, review date, language and hotel response. Hotels that fail add one uncharged item with a status and message.

# 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 = {
    "hotelUrls": [
        "https://www.booking.com/hotel/gb/the-savoy.html",
        "https://www.agoda.com/the-peninsula-hong-kong/hotel/hong-kong-hk.html"
    ],
    "maxReviewsPerHotel": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("heyitsgautham/hotel-reviews-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 = {
    "hotelUrls": [
        "https://www.booking.com/hotel/gb/the-savoy.html",
        "https://www.agoda.com/the-peninsula-hong-kong/hotel/hong-kong-hk.html",
    ],
    "maxReviewsPerHotel": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("heyitsgautham/hotel-reviews-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 '{
  "hotelUrls": [
    "https://www.booking.com/hotel/gb/the-savoy.html",
    "https://www.agoda.com/the-peninsula-hong-kong/hotel/hong-kong-hk.html"
  ],
  "maxReviewsPerHotel": 20
}' |
apify call heyitsgautham/hotel-reviews-scraper --silent --output-dataset

```

## MCP server setup

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