# Tripadvisor Hotel Scraper (`mrdoe/tripadvisor-hotel-scraper`) Actor

Extract hotel data from TripAdvisor anonymously — no login needed. Search hotels by name, city or URL; pull real per-provider OTA rates (Booking.com, Expedia, Agoda & more) side by side; or get a city's total hotel count and listing. Reads TripAdvisor's own embedded data, not guessed-at page text.

- **URL**: https://apify.com/mrdoe/tripadvisor-hotel-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.80 / 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 TripAdvisor Hotel Scraper do?

**TripAdvisor Hotel Scraper** extracts hotel data from [TripAdvisor](https://www.tripadvisor.com) - **anonymously, no login, no TripAdvisor account**. Three operations live in one Actor:

- **Hotel Search** - look up one or many hotels by name, by city, or by a direct TripAdvisor hotel URL/ID.
- **Room & OTA Rate Details** - the same lookup, plus every real per-provider rate TripAdvisor exposes for that hotel (Booking.com, Expedia, Hotels.com, Agoda, Priceline, Vio.com, and more - whatever OTA partners TripAdvisor has aggregated for that hotel at fetch time).
- **Location Hotel Count & List** - given just a city, how many hotels TripAdvisor lists there and a capped, paginated list of them.

### Why use this Actor?

- **Compare hotels** - batch several hotel names/URLs in one run (`hotelQueries`) to line up hotel X against hotel Y in one dataset.
- **Rate-shop across OTAs** - `roomRates` returns every provider's price for a hotel side by side (Booking.com vs. Expedia vs. Agoda...), including which one TripAdvisor flags as its lowest price.
- **Market sizing** - `location` tells you how many properties TripAdvisor lists for a city and hands you a sample.

### How to use it

1. Pick an **Operation**: Hotel Search, Room & OTA Rate Details, or Location Hotel Count & List.
2. Fill the matching field:
   - **Hotel Search / Room & OTA Rate Details** → `hotelQuery` (a hotel name, a city name, a full TripAdvisor `Hotel_Review-...` URL, or a bare numeric hotel ID) or `hotelQueries` for a batch.
   - **Location Hotel Count & List** → `location` (a city name or numeric geo ID) or `locations` for a batch.
3. Set `maxItems`.
4. Click **Start**.

Batch fields (`hotelQueries`/`locations`) win over their singular counterparts when filled; duplicates and blanks are dropped automatically.

### Input

| Field                         | Type              | Description                                                                                                  |
| ----------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------ |
| `operation`                   | string (required) | One of: `search`, `roomRates`, `location`.                                                                   |
| `hotelQuery` / `hotelQueries` | string / array    | A hotel name, city name, TripAdvisor `Hotel_Review` URL, or numeric hotel ID. Used by `search`/`roomRates`.  |
| `location` / `locations`      | string / array    | A city name or raw TripAdvisor numeric geo ID. Used by `location`.                                           |
| `maxItems`                    | integer           | Caps the number of hotel-summary rows produced per run. Default 10. No hard upper limit beyond what you set. |
| `proxyConfiguration`          | object            | **Required, residential.** TripAdvisor is DataDome-protected and blocks datacenter IPs outright.             |

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

A TripAdvisor hotel URL (e.g. `https://www.tripadvisor.com/Hotel_Review-g60763-d224214-Reviews-The_Bryant_Park_Hotel-New_York_City_New_York.html`) or a bare numeric hotel ID (the `d` number, e.g. `224214`) resolves directly and reliably to one hotel - TripAdvisor itself redirects a placeholder-slug URL built from just the geo+hotel IDs to the real canonical page, so no slug lookup is needed.

Free text (a hotel name and/or a city) is resolved through the **same curated city list** used by the `location` operation (see below), then optionally narrowed to a hotel-name match within that city's listing. This is not a global hotel-name search across all of TripAdvisor - it only works for hotels in a supported city (TripAdvisor's own search page is a client-side-rendered shell with no working anonymous free-text/typeahead endpoint found during development). For a hotel in an unsupported city, use the direct TripAdvisor URL/ID instead.

#### Supported city names (location text resolution)

Live-verified 2026-08-28 (10/10 checked against real TripAdvisor pages): Amsterdam, Bangkok, Barcelona, Berlin, Boston, Chicago, Dubai, Hong Kong, Istanbul, Las Vegas, London, Los Angeles, Miami, New York City, Orlando, Paris, Rome, San Francisco, Seoul, Singapore, Sydney, Tokyo, Toronto, Washington DC.

For any other city, pass its raw numeric TripAdvisor geo ID instead (visit `tripadvisor.com`, search the city, and copy the number after `Hotels-g` in the resulting URL) - `location`/`hotelQuery` both accept a bare numeric ID directly.

### Output

One flat row per item, distinguished by `kind`:

- `hotelSummary` - one per hotel (all three operations).
- `rate` - one per OTA-provider offer, `roomRates` only (a hotel with 12 connected providers produces 12 `rate` rows).
- `locationSummary` - one per city query, `location` only, carries the total hotel count.

Every row has `_operation` and `status` (`success`, `unavailable`, or `error`).

Hotel summary row:

```json
{
    "_operation": "search",
    "status": "success",
    "kind": "hotelSummary",
    "geoId": "60763",
    "hotelId": "224214",
    "hotelName": "The Bryant Park Hotel",
    "address": "40 West 40th Street, New York City, New York, 10018-2602",
    "city": "New York City",
    "country": "US",
    "phone": "1 (212) 869-0100",
    "latitude": 40.752895,
    "longitude": -73.983696,
    "rating": 4.8,
    "reviewCount": 5560,
    "priceFrom": 368,
    "priceMax": 1201,
    "url": "https://www.tripadvisor.com/Hotel_Review-g60763-d224214-Reviews-The_Bryant_Park_Hotel-New_York_City_New_York.html"
}
```

Rate row (`roomRates`, one per OTA provider):

```json
{
    "_operation": "roomRates",
    "status": "success",
    "kind": "rate",
    "hotelId": "224214",
    "hotelName": "The Bryant Park Hotel",
    "offerCount": 12,
    "provider": "Booking.com",
    "price": 570,
    "currency": "USD",
    "strikeThroughPrice": null,
    "isLowestPrice": false,
    "offerStatus": "AVAILABLE",
    "freeCancellation": true
}
```

Location summary row (`location`):

```json
{
    "_operation": "location",
    "status": "success",
    "kind": "locationSummary",
    "query": "New York City",
    "geoId": "60763",
    "hotelCount": 1038,
    "hotelsListed": 10
}
```

### How it works

TripAdvisor is protected by [DataDome](https://datadome.co/): a bare anonymous GET, a plain `got-scraping` request through a residential proxy, and headed/headless Chromium via plain Playwright were all live-tested 2026-08-28 and every one came back the same DataDome challenge shell (`geo.captcha-delivery.com`, "Please enable JS and disable any ad blocker"). [Camoufox](https://camoufox.com/) (a fingerprint-patched Firefox build) with its `humanize` option enabled, plus a homepage warm-up navigation before the target page in the same browser session, is the only tier that got through at all during development - see [Limitations](#limitations) for the honest, observed reliability of that bypass (it is inconsistent, not solved).

Once a page loads, hotel data is pulled from TripAdvisor's own embedded structures rather than guessed at from visible text:

- **Hotel summary** (`search`/`roomRates`/`location`): the detail page's `LodgingBusiness` JSON-LD block (name, address, phone, coordinates, aggregate rating) plus a `priceRange` figure pulled from the same hydration payload as the OTA offers.
- **OTA rate offers** (`roomRates`): TripAdvisor embeds its real-time rate-comparison data (the "top offers" row plus the longer "all deals" list) as a URL-percent-encoded, backslash-escaped JSON blob inside a `<script src="data:text/javascript,...">` hydration payload on every hotel detail page. This Actor locates and decodes just that one script's content (not the whole page, to avoid unrelated `%` characters elsewhere breaking the decode) and parses the real provider/price/cancellation-policy data out of it.
- **Listing cards** (`search` free-text/`location`): TripAdvisor's listing-page CSS classes are content-hashed and change on every deploy, so cards are located by the one stable marker TripAdvisor does keep (`data-automation="hotel-card-title"`) and parsed with small regexes within each card's chunk, rather than fragile class-name selectors. Pagination beyond the first ~6-30 hotels uses TripAdvisor's own `oa<N>` offset URL segment (verified live: `Hotels-g60763-oa30-...` 30x-redirects to the real page-2 URL).

### Limitations

- **DataDome bypass reliability varies over a session's lifetime - read this before relying on this Actor for production data.** During development, Camoufox (`humanize` + a homepage warm-up) passed DataDome cleanly across 4 separate fresh browser sessions and 15+ further page loads reused within them. Later in the same development window it then failed 16 fresh-session attempts in a row (across two different residential-proxy providers). One real bug was found and fixed during that investigation (an accidental Chrome-UA override that broke Camoufox's own Firefox fingerprint consistency), plus a second bug where the wrong hotel's link could get matched from deep inside a card's markup (also fixed - see the extraction bugs below). After both fixes, four separate platform runs against Apify's own Residential proxy group all succeeded cleanly (`location`, `roomRates`, a multi-hotel `search` batch, and a final confirmation run), each returning real hotel/rate data with no retries needed. **Net assessment: the bypass works, and worked repeatedly and cleanly in final verification - but DataDome's response to this technique is not perfectly stable over time, so occasional runs may still land on `status: "error"` rows if every retry attempt hits a blocked session.** This Actor automatically detects a blocked/challenged fetch and retries with a brand-new browser session (a fresh proxy exit IP), up to 3 attempts per page; if every retry is exhausted, that lookup lands as a `status: "error"` row rather than failing the whole run.
- **Two real extraction bugs were caught and fixed via live platform data** (not just local fixtures) during development: (1) a listing card's hotel name/ID could rarely be pulled from an unrelated `Hotel_Review` link elsewhere in the same card's markup rather than its own - fixed by narrowing the link search to the anchor immediately following the card's title marker; (2) some TripAdvisor sort orders (e.g. "Best Value") prefix a card's title with its own rank ("1. Hotel Name") - stripped as noise. Both are covered by regression tests in `test/main.test.js`.
- **`hotelCount` (the total-properties figure in a `locationSummary` row) is not always present.** It's parsed from a specific "N properties in \<City>" sentence TripAdvisor renders on page 1 of a listing; on at least one live run (London) that sentence wasn't present in the fetched markup and `hotelCount` came back `null`, while `hotelsListed` (the actual capped list this Actor returns) was still correct. Treat `hotelCount` as best-effort.
- **Free-text hotel-name search is city-scoped, not global.** No working TripAdvisor hotel-name-autocomplete/search endpoint could be reached anonymously (TripAdvisor's own `/Search` page is a client-rendered shell, and its internal `/TypeAheadJson` endpoint 403s outside a full browser challenge). Free text resolves via the curated city list, then narrows within that city's listing. Use a direct hotel URL/ID for a reliable single-hotel lookup outside the curated cities.
- **City-name resolution covers a curated list of 24 major cities**, live-verified against real TripAdvisor pages, not an exhaustive directory. Anything else needs the raw numeric geo ID from a TripAdvisor URL.
- **OTA rate offers depend on what TripAdvisor itself has aggregated for that hotel/date range at fetch time.** TripAdvisor does not expose a check-in/check-out date input on this anonymous path (no session/cart state), so offers reflect TripAdvisor's own default near-term date window, not a date range you choose. A hotel with no connected OTA partners for that window returns a `status: "unavailable"` `rate` row instead of provider prices.
- **A city listing page reliably renders ~6 main-list hotel cards per fetch** (plus TripAdvisor's own separate "top picks" carousel, not extracted here to keep the data shape consistent); more are reached via TripAdvisor's own page-offset pagination (`oa<N>`), which this Actor uses automatically up to `maxItems`.
- Prices reflect TripAdvisor's own currency/pricing at fetch time and are not guaranteed to match what you'd see at checkout on the OTA's own site.

### Pricing

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

### FAQ

**Do I need a TripAdvisor account or API key?** No - it runs entirely against TripAdvisor's public, logged-out web pages.

**Why did I get 0 OTA rate offers for a hotel?** TripAdvisor had no connected/available provider rates for that hotel at fetch time (see [Limitations](#limitations)) - the hotel summary row is still returned.

**Can I pick my own check-in/check-out dates for rates?** Not anonymously - see [Limitations](#limitations). TripAdvisor's rate-comparison data on this path reflects its own default date window.

### Disclaimer

This Actor is an independent tool, not affiliated with or endorsed by TripAdvisor. It only accesses data TripAdvisor serves publicly to anonymous visitors. Use it in compliance with TripAdvisor'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

## `operation` (type: `string`):

search = look up hotel(s) by name/location text or a direct TripAdvisor URL/ID, one dataset row per hotel. roomRates = the same lookup plus every real per-OTA-provider rate TripAdvisor exposes for that hotel (Booking.com, Expedia, Hotels.com, Agoda, Priceline, Vio.com...). location = count and list the hotels TripAdvisor has for a city, with no specific hotel in mind.

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

Used by "search" and "roomRates". A hotel name, a city name (see README for the supported city list), a full TripAdvisor hotel URL (Hotel\_Review-...), or a bare numeric hotel ID (the "d" number in that URL). Defaults to a demo query if left blank.

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

Many hotel names, city names, URLs, or hotel IDs in one run - e.g. to compare hotel X and hotel Y. One dataset row per hotel. Wins over "hotelQuery" when filled.

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

Used by "location". A supported city name (see README) or a raw TripAdvisor numeric geo ID (the "g" number in a TripAdvisor URL). Defaults to a demo city if left blank.

## `locations` (type: `array`):

Many cities in one run. Wins over "location" when filled.

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

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

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

A residential proxy is required - TripAdvisor is protected by DataDome and blocks datacenter IPs outright.

## Actor input object example

```json
{
  "operation": "search",
  "hotelQuery": "The Bryant Park Hotel, New York City",
  "location": "New York City",
  "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": "The Bryant Park Hotel, New York City",
    "location": "New York City"
};

// Run the Actor and wait for it to finish
const run = await client.actor("mrdoe/tripadvisor-hotel-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": "The Bryant Park Hotel, New York City",
    "location": "New York City",
}

# Run the Actor and wait for it to finish
run = client.actor("mrdoe/tripadvisor-hotel-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": "The Bryant Park Hotel, New York City",
  "location": "New York City"
}' |
apify call mrdoe/tripadvisor-hotel-scraper --silent --output-dataset

```

## MCP server setup

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