# Hotel Reviews Scraper (Booking.com & Agoda) (`xtracto/hotel-reviews`) Actor

Scrape guest reviews for a hotel from Booking.com or Agoda — rating, review text, author nationality, stay date, room type, and the hotel's response. No login.

- **URL**: https://apify.com/xtracto/hotel-reviews.md
- **Developed by:** [Farhan Febrian Nauval](https://apify.com/xtracto) (community)
- **Categories:** Travel, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 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.
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)

Extract guest reviews for a hotel from Booking.com or Agoda — rating, review text, the guest's nationality, stay dates, room type, and the hotel's written response. No account or login required.

### Why use this actor

- **No account, login, or API key required.**
- **Two sources, one actor** — pick Booking.com or Agoda per run.
- **Rich per-review detail** — rating, title, what the guest liked/disliked, room booked, stay dates, traveler type, and helpful-vote count.
- **The hotel's reply is included** — when a hotel responds to a review, that response text and date come through too.
- **Agoda reviews are pre-aggregated** across Agoda's own guests and reviews originally left on Booking.com, so you get broader coverage from a single call.
- **Stable JSON output** ready for spreadsheets, dashboards, databases, or sentiment pipelines.
- Runs on a schedule, retries automatically, and exports to JSON, CSV, or Excel.

### How it works

1. Pick a **source** (Booking.com or Agoda) and paste the hotel's page URL — for Agoda you can paste the URL or just its numeric property ID.
2. Choose how you want reviews sorted (newest, oldest, highest/lowest rating, most relevant) and set a cap per hotel and for the whole run.
3. Run it. The actor opens each hotel's review list and pushes one clean record per review, working through pages until it hits your cap or runs out of reviews.

You do not manage any scrapers, browsers, or blocks — the actor handles that automatically and returns clean records.

### Input

Booking.com:

```json
{
  "source": "booking",
  "hotels": ["https://www.booking.com/hotel/id/ktm-resort-batam.html"],
  "sortBy": "newest",
  "maxReviewsPerHotel": 100,
  "maxReviews": 500,
  "solveAttempts": 5,
  "proxyConfiguration": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"] }
}
```

Agoda (URL or numeric property ID both work):

```json
{
  "source": "agoda",
  "hotels": ["11295575"],
  "sortBy": "highest_rating",
  "maxReviewsPerHotel": 100,
  "maxReviews": 500,
  "proxyConfiguration": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"] }
}
```

| Field | Type | Description |
|-------|------|-------------|
| `source` | string | `booking` or `agoda`. |
| `hotels` | array | Booking: the hotel's Booking.com page URL(s). Agoda: the hotel's Agoda page URL(s), or its numeric Agoda property ID. |
| `sortBy` | string | `newest`, `oldest`, `highest_rating`, `lowest_rating`, or `most_relevant`. Agoda has no separate "oldest first", so it falls back to newest. |
| `maxReviewsPerHotel` | integer | Cap on reviews pulled per hotel. `0` = all available. |
| `maxReviews` | integer | Overall cap for the run, across all hotels. `0` = no limit. |
| `solveAttempts` | integer | Booking.com only — how many times to retry the initial unlock on a fresh IP. More = more reliable. |
| `proxyConfiguration` | object | Residential is recommended (default). |

### Output

One record per review.

Booking.com:

```json
{
  "_input": "https://www.booking.com/hotel/id/ktm-resort-batam.html",
  "_source": "S1-booking-reviewlist",
  "_scrapedAt": "2026-08-06T10:05:23Z",
  "recordType": "REVIEW",
  "source": "booking",
  "hotelName": "KTM Resort Batam",
  "hotelUrl": "https://www.booking.com/hotel/id/ktm-resort-batam.html",
  "reviewId": "63a25c73ebdad386",
  "authorName": "Poovendran",
  "authorNationality": "Malaysia",
  "authorCountryCode": "my",
  "rating": 8.0,
  "reviewTitle": "Stay in Ktm Resort",
  "positiveText": "Breakfast buffet and friendly staff",
  "negativeText": "A bit far from the main city, distance wise it's near but too many cars in the road making it more than half an hour to go to the city",
  "reviewText": null,
  "roomType": "Deluxe Villa",
  "travelerType": "Solo traveler",
  "stayCheckIn": "2026-07-23",
  "stayCheckOut": "2026-07-26",
  "numNights": 3,
  "reviewDate": "2026-07-30T02:00:28Z",
  "hotelResponse": null,
  "hotelResponseDate": null,
  "helpfulVotes": 0,
  "language": "en"
}
```

Agoda (with a hotel response):

```json
{
  "_input": "11295575",
  "_source": "S1-agoda-hotelreviews",
  "_scrapedAt": "2026-08-06T10:05:59Z",
  "recordType": "REVIEW",
  "source": "agoda",
  "hotelName": "Desa Potato Head Bali",
  "hotelUrl": null,
  "reviewId": 1127366899,
  "authorName": "Rebecca",
  "authorNationality": "Australia",
  "authorCountryCode": "au",
  "rating": 10.0,
  "reviewTitle": "Fantastic stay",
  "positiveText": null,
  "negativeText": null,
  "reviewText": "We had a great stay at potato head, staying in one of their ocean front rooms. The staff and service were wonderful and really attentive. Will be back! ",
  "roomType": "Oceanfront Studio",
  "travelerType": "Couple",
  "stayCheckIn": "2026-05-30T00:00:00+07:00",
  "stayCheckOut": "2026-06-01T00:00:00+07:00",
  "numNights": 2,
  "reviewDate": "2026-06-03T05:00:00+07:00",
  "hotelResponse": "Dear Rebecca, \n\nThank you for your wonderful review! We are delighted to hear that you had a great stay with us and enjoyed your Ocean Front room... (truncated)",
  "hotelResponseDate": "2026-06-03T18:55:13.253+07:00",
  "helpfulVotes": 0,
  "language": null,
  "reviewSourceProvider": "Agoda"
}
```

| Field | Type | Description |
|-------|------|-------------|
| `hotelName` | string | Hotel name. |
| `hotelUrl` | string | The Booking.com page URL you provided (`null` for Agoda, which is looked up by numeric ID). |
| `reviewId` | string/number | Unique id for the review. |
| `authorName` | string | Reviewer's display name (`null` if they reviewed anonymously). |
| `authorNationality` | string | Reviewer's country, as shown on the review. |
| `authorCountryCode` | string | Two-letter country code for the flag icon shown next to the reviewer. |
| `rating` | number | Score out of 10. |
| `reviewTitle` | string | Headline the guest gave their review, if any. |
| `positiveText` | string | What the guest liked (Booking splits pros/cons this way; Agoda only for reviews written that way). |
| `negativeText` | string | What the guest disliked. |
| `reviewText` | string | Free-form review body, used when the source doesn't split it into pros/cons (mainly Agoda). |
| `roomType` | string | Room type the guest stayed in, when the source records it. |
| `travelerType` | string | e.g. "Solo traveler", "Couple", "Family". |
| `stayCheckIn` / `stayCheckOut` | string | Dates of the reviewed stay. |
| `numNights` | integer | Length of stay in nights. |
| `reviewDate` | string | When the review was posted. |
| `hotelResponse` | string | The hotel's written reply to the review, if any. |
| `hotelResponseDate` | string | When the hotel replied (Agoda only). |
| `helpfulVotes` | integer | "Helpful" votes the review received, if the source shows one. |
| `reviewSourceProvider` | string | Agoda only — which platform the review was originally posted on (Agoda's aggregated reviews include ones sourced from Booking.com and other channels). |

### Notes & limits

- **Booking.com** unlocks the hotel's page once per run and reuses that unlock for every additional Booking hotel URL you add — only the first hotel (or a re-check after a long gap) pays the full unlock cost.
- **Sorting on Agoda:** there's no dedicated "oldest first" option on Agoda's side, so choosing `oldest` there falls back to the same ordering as `newest`.
- **Volume:** both sources page through as many reviews as are available, up to your caps. Very large hotels (thousands of reviews) will take longer and use more proxy bandwidth the higher you set `maxReviewsPerHotel`.
- **Proxy:** Residential is recommended and set as the default.

### Other actors

See the sibling scrapers in this collection — `booking-hotels-scraper` and `agoda-hotels-scraper` — for hotel search results and live room rates from the same two sites.

# Actor input Schema

## `source` (type: `string`):

Which site to pull guest reviews from.

## `hotels` (type: `array`):

For Booking.com: paste the hotel's Booking.com page URL(s), e.g. https://www.booking.com/hotel/id/ktm-resort-batam.html . For Agoda: paste the hotel's Agoda page URL(s), or its numeric Agoda property ID, e.g. 11295575.

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

How to order reviews before applying the per-hotel cap. Agoda has no separate 'oldest first' option, so it falls back to 'Newest'.

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

Cap on reviews pulled per hotel. 0 = all available reviews.

## `maxReviews` (type: `integer`):

Overall cap on review records for the whole run, across all hotels. 0 = no limit.

## `solveAttempts` (type: `integer`):

Booking.com only. How many times to retry the initial browser unlock on a fresh proxy IP if the site's bot check doesn't clear. More attempts = more reliable, slightly higher cost.

## `proxyConfiguration` (type: `object`):

Booking.com is IP-reputation-sensitive; RESIDENTIAL is recommended for both sources.

## Actor input object example

```json
{
  "source": "booking",
  "hotels": [
    "https://www.booking.com/hotel/id/ktm-resort-batam.html"
  ],
  "sortBy": "newest",
  "maxReviewsPerHotel": 100,
  "maxReviews": 500,
  "solveAttempts": 5,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `hotelName` (type: `string`):

Hotel Name as reported by the source.

## `authorName` (type: `string`):

Author Name as reported by the source.

## `authorNationality` (type: `string`):

Author Nationality as reported by the source.

## `rating` (type: `string`):

Rating score. Numeric value.

## `reviewTitle` (type: `string`):

Review Title as reported by the source.

## `positiveText` (type: `string`):

Positive Text as reported by the source.

## `negativeText` (type: `string`):

Negative Text as reported by the source.

## `roomType` (type: `string`):

Room Type as reported by the source.

## `stayCheckIn` (type: `string`):

Stay Check In as reported by the source.

## `reviewDate` (type: `string`):

Review Date as reported by the source.

## `hotelResponse` (type: `string`):

Hotel Response as reported by the source.

## `recordType` (type: `string`):

Which kind of row this is.

## `hotelUrl` (type: `string`):

Link to the hotel.

## `reviewId` (type: `string`):

Review Id as reported by the source.

## `authorCountryCode` (type: `string`):

Author Country Code as reported by the source.

## `reviewText` (type: `string`):

Review Text as reported by the source.

## `travelerType` (type: `string`):

Traveler Type as reported by the source.

## `stayCheckOut` (type: `string`):

Stay Check Out as reported by the source.

## `numNights` (type: `string`):

Num Nights.

## `hotelResponseDate` (type: `string`):

Hotel Response Date as reported by the source.

## `helpfulVotes` (type: `string`):

Helpful Votes.

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

Language code of the item.

## `reviewSourceProvider` (type: `string`):

Review Source Provider as reported by the source.

## `_input` (type: `string`):

The input value this row was produced from.

## `_source` (type: `string`):

Which extraction strategy produced the row.

## `_scrapedAt` (type: `string`):

UTC timestamp of the scrape, ISO 8601.

## `_error` (type: `string`):

Set only on diagnostic rows - why that target produced no data.

## `_errorDetail` (type: `string`):

Extra context for the error.

# 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 = {
    "source": "booking",
    "hotels": [
        "https://www.booking.com/hotel/id/ktm-resort-batam.html"
    ],
    "sortBy": "newest"
};

// Run the Actor and wait for it to finish
const run = await client.actor("xtracto/hotel-reviews").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 = {
    "source": "booking",
    "hotels": ["https://www.booking.com/hotel/id/ktm-resort-batam.html"],
    "sortBy": "newest",
}

# Run the Actor and wait for it to finish
run = client.actor("xtracto/hotel-reviews").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 '{
  "source": "booking",
  "hotels": [
    "https://www.booking.com/hotel/id/ktm-resort-batam.html"
  ],
  "sortBy": "newest"
}' |
apify call xtracto/hotel-reviews --silent --output-dataset

```

## MCP server setup

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

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/FO0KXUHtsZnavQtg7/builds/eCiAMXVpDQ6wvlsMD/openapi.json
