# Agoda Review Scraper (`mrdoe/agoda-review-scraper`) Actor

Extract guest reviews from Agoda.com hotel pages anonymously without a login. Input a hotel name, URL, or property ID to get comprehensive hotel summaries and individual review data (ratings, text, stay dates, reviewer country, traveler type) fed directly into a structured dataset for analysis.

- **URL**: https://apify.com/mrdoe/agoda-review-scraper.md
- **Developed by:** [MrDoe](https://apify.com/mrdoe) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.70 / 1,000 results

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

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

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

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

### What does Agoda Review Scraper do?

**Agoda Review Scraper** extracts guest reviews from [Agoda](https://www.agoda.com) hotel pages - **anonymously, no login, no Agoda account**. Give it a hotel name, a full agoda.com URL, or a numeric property ID, and it returns a hotel-summary row (overall score, total review count) plus individual review rows (rating, title, full text, positives/negatives, reviewer country/traveler type, stay dates, helpful votes).

### Why use this Actor?

- **Reputation monitoring** - pull a hotel's latest reviews on a schedule and track sentiment/rating trends over time.
- **Competitive research** - batch several competing hotels (`hotelQueries`) and compare review volume, score, and guest sentiment in one dataset.
- **Voice-of-guest analysis** - full review text plus separately-tagged positives/negatives feed straight into text analysis without extra parsing.

### How to use it

1. Set `hotelQuery` (a hotel name, a full agoda.com hotel URL, or a bare numeric property ID) or `hotelQueries` for a batch.
2. Pick `sortBy` (defaults to Most Helpful, matching agoda.com's own default).
3. Set `maxItems` to cap the number of review rows.
4. Click **Start**.

`hotelQueries` wins over `hotelQuery` when filled; duplicates and blanks are dropped automatically.

### Input

| Field                | Type    | Description                                                                                                |
| -------------------- | ------- | ---------------------------------------------------------------------------------------------------------- |
| `hotelQuery`         | string  | A hotel name, a full agoda.com hotel URL, or a bare numeric propertyId. Defaults to a demo hotel if blank. |
| `hotelQueries`       | array   | Many hotel names/URLs/IDs in one run. Wins over `hotelQuery` when filled.                                  |
| `sortBy`             | string  | `mostHelpful` (default), `mostRecent`, `ratingHighToLow`, or `ratingLowToHigh`.                            |
| `maxItems`           | integer | Caps the number of review rows produced per run (across all hotel queries). Default 10.                    |
| `proxyConfiguration` | object  | A residential proxy is recommended for sustained/high-volume usage.                                        |

#### Resolving a hotel: URL/ID vs. free text

A full agoda.com hotel-page URL (e.g. `https://www.agoda.com/marina-bay-sands/hotel/singapore-sg.html`) resolves directly - the numeric propertyId is pulled out of the page's raw HTML. A bare numeric ID is used as-is. Free text (a hotel name) is resolved through Agoda's own public autocomplete API (`GetUnifiedSuggestResult`) - the same one that powers the search box on agoda.com.

### Output

One `hotelSummary` row per hotel query, followed by its `review` rows, distinguished by `kind`. Every row has `status` (`success` or `error`).

Hotel summary row:

```json
{
    "status": "success",
    "kind": "hotelSummary",
    "query": "Marina Bay Sands",
    "hotelId": "185945",
    "hotelName": "Marina Bay Sands",
    "overallScore": 8.9,
    "overallReviewCount": 48872
}
```

Review row:

```json
{
    "status": "success",
    "kind": "review",
    "query": "Marina Bay Sands",
    "hotelId": "185945",
    "hotelName": "Marina Bay Sands",
    "reviewId": "1162245625",
    "rating": 10,
    "ratingText": "Exceptional",
    "title": "The best memories",
    "comment": "During our trip to Singapore, it was my son's birthday, so we received a complimentary room upgrade...",
    "positives": null,
    "negatives": null,
    "reviewDate": "2026-08-14T04:35:00+07:00",
    "checkInDate": "2026-08-13T00:00:00+07:00",
    "checkOutDate": "2026-08-14T00:00:00+07:00",
    "lengthOfStay": 1,
    "reviewerName": "AI",
    "reviewerCountry": "Japan",
    "travelerType": "Family with young children",
    "roomTypeName": "Sands Premier Double Queen Room",
    "helpfulVotes": 0,
    "unhelpfulVotes": 0,
    "hasHotelResponse": false,
    "responderName": null,
    "provider": "Agoda"
}
```

### How it works

Agoda's hotel-review page is a client-rendered React shell - its raw HTML has empty content divs. Rather than driving a browser to render it, this Actor calls the same internal REST API the page's own JavaScript calls (`cronos/property/review/HotelReviews`) directly with a plain HTTP POST (`got-scraping`, no headless browser) and paginates through it with `pageNo`. Live-verified 2026-08-28: a bare replay of the request body alone returned zero reviews (200 OK, empty array) until the browser's exact extra headers were matched (`x-requested-with: XMLHttpRequest`, `accept: application/json`, the `cr-currency-*`/`ag-language-*` headers) - with those, a plain cookie-only request returns full review text, ratings, reviewer info, and metadata. No CAPTCHA, DataDome, PerimeterX, or TLS-fingerprint check was hit against this endpoint. A single cookie-collecting request is made once per run and its cookies reused across calls.

### Limitations

- **`pageSize` above ~25 was observed to silently return zero reviews** during recon - this Actor always requests 25 per page and paginates instead of asking for a larger page. Pagination stops automatically once a page returns no reviews not already seen, or after 40 pages as a safety cap.
- **No hotel-response body text is exposed by this API** - only whether a response exists (`hasHotelResponse`) and the responder's name, not the response's own text.
- **Free-text search depends on Agoda's own autocomplete matching your text.** For an obscure/ambiguous hotel name, pass a direct agoda.com URL or numeric propertyId instead for a reliable match.
- Review order and content reflect Agoda's own review platform at fetch time (Agoda aggregates reviews collected across several OTA partners under one property).
- **Review text language can vary with the proxy's exit-IP geolocation.** Agoda's translation behavior appears to weigh the request's apparent country alongside the `ag-language-locale` header - a review may come back in its original language (with `originalTitle`/`originalComment` duplicating it) rather than translated to English, depending on where the request looks like it's coming from.

### Pricing

This Actor uses pay-per-event pricing - one small run-start charge, then a per-row charge for each review row. See the **Pricing** tab for current rates. Failed lookups land as `status: "error"` rows and are never charged.

### FAQ

**Do I need an Agoda account or API key?** No - it runs entirely against Agoda's public, logged-out internal API.

**Why did I get a `hotelSummary` row but no `review` rows?** The hotel may genuinely have no reviews yet, or Agoda's review API returned an empty page for this property/sort combination - try a different `sortBy`.

**Can I get the hotel's written response to a review?** Not the response text itself - see [Limitations](#limitations). Only whether a response exists and who wrote it.

### Disclaimer

This Actor is an independent tool, not affiliated with or endorsed by Agoda. It only accesses data Agoda serves publicly to anonymous visitors. Use it in compliance with Agoda's terms of use and applicable law in your jurisdiction. Found a bug or have a feature request? Use the Issues tab on this Actor's page.

# Actor input Schema

## `hotelQuery` (type: `string`):

A hotel name, a full agoda.com hotel URL, or a bare numeric propertyId. Defaults to a demo hotel if left blank.

## `hotelQueries` (type: `array`):

Many hotel names, URLs, or propertyIds in one run. Wins over "hotelQuery" when filled.

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

The order reviews are fetched and paginated in - matches the sort options on agoda.com's own review page.

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

Caps the number of review rows produced per run (across all hotel queries). No hard upper limit is enforced beyond what you set here.

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

A residential proxy is recommended for sustained/high-volume usage.

## Actor input object example

```json
{
  "hotelQuery": "Marina Bay Sands",
  "sortBy": "mostHelpful",
  "maxItems": 10,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

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

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

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

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "hotelQuery": "Marina Bay Sands"
};

// Run the Actor and wait for it to finish
const run = await client.actor("mrdoe/agoda-review-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 = { "hotelQuery": "Marina Bay Sands" }

# Run the Actor and wait for it to finish
run = client.actor("mrdoe/agoda-review-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 '{
  "hotelQuery": "Marina Bay Sands"
}' |
apify call mrdoe/agoda-review-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,mrdoe/agoda-review-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/JH6e5SMJKDNrk7Qps/builds/XxE4rKy0bFnHrrMeC/openapi.json
