# Trip.com Hotels, Rooms & Reviews Scraper (`abotapi/trip-com-scraper`) Actor

Pull structured hotel listings from trip.com with prices, room types, amenities, policies, nearby places, and full guest reviews with ratings breakdown and an AI review summary. Search by destination or paste trip.com URLs directly.

- **URL**: https://apify.com/abotapi/trip-com-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Features:** Limited permissions
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 hotel 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?

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

## Trip.com Hotels Scraper: Prices, Rooms & Full Guest Reviews

Trip.com Hotels Scraper turns Trip.com hotel listings into structured data: name, star rating, guest review score, price (current, original and with taxes and fees), images, amenities, check in and check out policies, nearby places, room types with per room pricing, and the full guest review feed with a ratings breakdown and an AI generated review summary. Search by destination name, or paste Trip.com hotel links straight from your browser (every link shape works), then export to JSON, CSV or Excel, or read the results through the API.

### Why This Scraper?

- **Real destination search.** Type "Tokyo" or "New York" - no city ids to hunt for. Trip.com city ids and `id|Name` strings still work for power users.
- **Every Trip.com link works in URL mode.** Hotel list links, hotel detail links, and the SEO links search engines show (like `/hotels/tokyo-hotels-list-228/`) all resolve - missing dates and guests are filled from your input automatically.
- **Full hotel record in one run.** Price (current, original, with taxes and fees), star rating, guest review score, images, amenities, policies, nearby places and a description, all in a single dataset row per hotel.
- **Room level detail and the full guest review feed, not just an average.** Rooms nest per hotel with cancellation terms and per room pricing; reviews carry per category ratings, traveler type and useful count, plus an aggregate breakdown and an AI generated summary. Cap the feed with Max reviews (default 20).
- **Filters that don't starve later pages.** Minimum star rating and minimum guest review score are applied before any per page item cap, and each destination keeps being resampled until it stops producing hotels you haven't already collected.
- **Incremental mode for schedules.** Let the actor remember what it already delivered and push only what is new or changed since the last run, with `changeType` on every row.
- **Pay per hotel returned.** One detail-enrichment unit applies per enriched hotel pushed (reviews add no further units); no flat monthly fee.

### Use Cases

- **Travel agencies and OTAs:** benchmark Trip.com's pricing and availability against your own listings for a destination and date range.
- **Rate and demand analysts:** track price, discount and occupancy signals across many hotels in a city over time - incremental mode makes the scheduled re-runs cheap to read.
- **Reputation and review teams:** pull the full guest review feed and AI summary to spot recurring complaints or praised amenities.
- **Travel content and comparison sites:** build destination pages with live pricing, star ratings and photos.
- **Revenue managers:** compare your own property's room types and cancellation terms against nearby competitors listed on Trip.com.

### Data You Get

> Sample shape: values are illustrative placeholders, not from a live record.

| Field | Example |
| --- | --- |
| `hotelId` | `"0000000"` |
| `name` | `"Sample Grand Hotel"` |
| `detailUrl` | Trip.com link to the hotel's detail page |
| `starRating` | `4` |
| `reviewScore` / `reviewScoreLabel` | `9.1` / `"Great"` |
| `reviewCount` | `1200` |
| `reviewTags` | `["Easy to get around", "Delicious breakfast"]` |
| `address` | `"Sample District, Sample City"` |
| `lat` / `lng` | `35.0000` / `139.0000` |
| `priceCurrent` / `priceOriginal` | `120` / `140` |
| `priceTotalWithTaxes` | `134` |
| `discountPercent` / `discountLabel` | `14` / `"14% off"` |
| `currency` | `"USD"` |
| `images` / `image` | full photo list / the first photo |
| `amenities` | `["Free WiFi", "Fitness center", "Parking"]` |
| `policies` | check in, check out and cancellation terms |
| `nearbyPlaces` | `[{"name": "Sample Station", "type": "station", "distance": "400m"}]` |
| `rooms` / `roomCount` | per room name, meal notes, price and cancellation terms / how many |
| `reviews` | individual guest reviews with a per category rating breakdown |
| `aggregateRatingBreakdown` | `{"overall": 9.1, "location": 9.3, "facility": 8.9, "service": 9.0, "room": 9.0}` |
| `aggregateAiSummary` | AI generated summary of what guests say |
| `changeType` / `changedFields` / `firstSeenAt` / `lastSeenAt` | incremental mode only: what changed since the last run |
| `scrapedAt` | `"2026-11-01T00:00:00.000Z"` |

Each review also carries `reviewHotelId`, `reviewRoomName`, `reviewerCommentCount` (how many reviews the guest has written), `reviewSubScores` (location, facility, service, room scores) and, when the property replied, `reviewReplyContent`, `reviewReplyTranslatedContent`, `reviewReplyLanguage` and `reviewReplyDate`. These new review fields are left out of incremental change detection, so they never mark a hotel as updated.

Every record also carries `description`, `reviewCount_pushed` (reviews included in the record), `badges`, `medal`, `propertyTag`, `contactPhone`, `contactEmail`, `cityId`, `cityName`, `aggregateTotalReviewCount` and `aggregateCommentTags`. Three fields are always `null`: `roomImages` (no separate room gallery payload was found), `confirmInfo` (no post booking confirmation payload is exposed pre booking) and each review's legacy `reviewOwnerReply` (the property's reply is in the `reviewReply*` fields below). `nearbyHotels` is always an empty array for the same reason.

### How to Use

1. Pick a **mode**: `search` (destination and dates) or `url` (paste Trip.com links).
2. For search mode, type one or more destination names ("Tokyo", "New York") and set dates, guests and any star or review filters. For url mode, paste one or more Trip.com hotel links - list links, detail links, or the SEO links copied from a search engine all work.
3. Leave **Fetch hotel detail pages** on for amenities, policies, room pricing detail and reviews, or turn it off for just the listing page fields. Set **Max reviews** (default 20) to cap the review feed per hotel.
4. Set **Max pages** and **Max listings** to control run size and cost, then click **Start**. For scheduled runs, see **Incremental mode** below.

**Search by destination name:**

```json
{
  "mode": "search",
  "locations": ["Tokyo"],
  "checkIn": "2026-11-15",
  "checkOut": "2026-11-16",
  "adults": 2,
  "maxListings": 20,
  "fetchDetails": true
}
```

**Mix names and city ids with a star rating filter:**

```json
{
  "mode": "search",
  "locations": ["Tokyo", "359|Bangkok", "73"],
  "checkIn": "2026-12-01",
  "checkOut": "2026-12-04",
  "minStarRating": 4,
  "maxListings": 40
}
```

**Paste Trip.com links - every shape works:**

```json
{
  "mode": "url",
  "urls": [
    "https://www.trip.com/hotels/tokyo-hotels-list-228/",
    "https://www.trip.com/hotels/list?city=228&cityName=Tokyo&checkIn=2026-11-15&checkOut=2026-11-16",
    "https://www.trip.com/hotels/tokyo-hotel-detail-1234567/sample-hotel-name/"
  ],
  "fetchDetails": true
}
```

A pasted detail link returns the detail page fields; listing fields such as price, star rating, review score and images stay empty because they come from the list page.

**Scrape a single hotel’s full detail and reviews:**

```json
{
  "mode": "url",
  "urls": ["https://www.trip.com/hotels/detail/?hotelId=0000000&checkIn=2026-11-15&checkOut=2026-11-16"]
}
```

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/trip-com-scraper").call(run_input={"mode": "search", "locations": ["Tokyo"], "maxListings": 20})
for hotel in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(hotel["name"], hotel["priceCurrent"], hotel["currency"])
```

JavaScript:

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

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('abotapi/trip-com-scraper').call({ mode: 'search', locations: ['Tokyo'], maxListings: 20 });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

Or connect it to Make, Zapier, n8n, Google Sheets or webhooks from the **Integrations** tab.

#### Search mode: destination names

Type the destination the way you would say it - "Tokyo", "New York", "Seoul", "Ho Chi Minh City". Names are matched against a built-in index of Trip.com's own destinations (the same slugs and ids Trip.com uses in its links), so common spellings, aliases like "NYC", and capitalization all resolve with zero extra requests. Trip.com city ids are accepted too: a bare `228` or `228|Tokyo`.

If a smaller town is not in the index, the run log tells you exactly what to do: switch to URL mode and paste any Trip.com hotel link for that place - links copied from a search engine (like `/hotels/<town>-hotels-list-<id>/`) carry the city id and work directly.

#### URL mode: every link shape

| Link you paste | What the actor does |
| --- | --- |
| `/hotels/list?city=228&cityName=Tokyo&checkIn=...&checkOut=...&adult=2&crn=1&curr=USD` (canonical list search) | Scraped as-is; missing dates, guests or currency filled from your input; legacy spellings (`checkin`, `adults`) normalized. |
| `us.trip.com/hotels/list?city=359` (city only) | Dates, guests and currency filled from your input. |
| `/hotels/list?city=228&...&page=2` or `&sort=3&star=5` (paginated, sorted or filtered list link) | Accepted, but Trip.com ignores page, sort and star on a plain list fetch (measured: page 2 and the sorted 5-star link return the same kind of mixed-star slice as page 1), so these links are not in the example list. A link with a page number is treated as a partial scan, so incremental mode never expires hotels from it. Use Minimum star rating and Minimum guest review score to filter. |
| `/hotels/tokyo-hotels-list-228/` (SEO city link) | Converted to the canonical list link for city 228 with your dates and guests. |
| `au.trip.com/hotels/singapore-hotels-list-73/` (regional host: au, ae, at, br, ca, us, hk...) | Same as the SEO city link. |
| `/hotels/tokyo-asakusa/hotels-c228m15019878/` (landmark page) | Converted to the city list for city 228 (the landmark filter itself is not available to a plain fetch). |
| `/hotels/shanghai-ibis/hotels-c2b1137/` (brand page) | Converted to the city list for city 2 (the brand filter itself is not available to a plain fetch). |
| `/hotels/tokyo-jr-tokyo-station/hotels-c228r8254862/` (railway station page) | Converted to the city list for city 228 (the station filter itself is not available to a plain fetch). |
| `/hotels/detail/?hotelId=1677831` (hotel detail) | Scraped as-is; missing dates and guests filled from your input. |
| `/hotels/detail/?hotelId=686797&checkIn=...&adult=2&crn=1&curr=USD` (detail with dates and guests) | Scraped with your link's dates and guests. |
| `/hotels/tokyo-hotel-detail-993510/imperial-hotel-tokyo/` (SEO detail) | Converted to the canonical detail link for hotel 993510. |
| `/hotels/tokyo-hotel-detail-1489108/` (SEO detail without a hotel name) | The id decides the hotel, not the name. |
| `.../review.html` and `.../photo.html` (SEO detail sub-pages) | Converted to the canonical detail link for that hotel id. |
| Anything else (the `/hotels/` landing page, homepage, city directories, flights, `m.trip.com`, zone-suffixed city links, other hosts) | Skipped with a warning in the log - never misread as a hotel page. |

**Default behaviour:** if you switch to URL mode and leave **Trip.com URLs** untouched, the actor scrapes the example links above, one per link shape. Clear the field and paste your own links to scrape only those. A single `hotelUrl` key is also accepted in the input as an alias for one link. **Minimum star rating** and **Minimum guest review score** also apply to hotels from pasted links.

Multi-URL is supported. Each list link collects repeated result samples, as described below. The `us.` / `hk.` / other market hosts are all accepted.

#### How Max pages and Max listings shape a run

Trip.com's hotel list page does not expose a real page number to a plain fetch: each additional page instead re-queries the destination and returns a resampled slice of its full hotel pool (thousands of hotels for a city like Tokyo), with partial overlap between fetches. The actor keeps a hotel-id dedup set for the whole run and only counts genuinely new hotels toward your target. The actor stops a destination after several consecutive resamples bring back nothing new, since one empty resample alone has been observed to still be followed by a fresh one. `maxListings` is a run-wide total, split evenly across your destinations and list links.

That dedup set is shared across every destination and every pasted URL in the run, and it is keyed by hotel id only, not by dates or guests. This has three effects when two entries in one run cover the same city (the same city listed twice, two link variants of it, or, in URL mode, the same city with different dates or guest counts):

- The second entry only returns hotels the first one didn't. It can stop after a handful of hotels, well short of its share of `maxListings`, and in URL mode a second date range will not return a second price for hotels already collected under the first.
- If the first entry has already collected everything that city offers, the second entry never finds a new hotel, so the "nothing new" stop never triggers: it keeps re-fetching until **Max pages** is reached, or until the run times out if **Max pages** is empty.
- Hotels a page brings back but that are cut by the `maxListings` cap before being saved are still marked as collected, so no later entry in the same run returns them, including a pasted hotel detail link for one of those hotels, which is skipped.

Workaround: list each city only once and raise `maxListings` or `maxPages` instead of adding a duplicate entry. To compare prices for the same city across different dates, use one run per date range. If you must repeat a city in one run, set **Max pages**.

#### Incremental mode: scheduled runs that only return what changed

Turn **Incremental mode** on and the actor remembers the hotels it delivered for your exact search scope (destinations or URLs, dates, guests, currency, filters and review cap - or name the scope yourself with **State key**). On each scheduled run every hotel is classified against that memory:

- `NEW` - first time seen; pushed.
- `UPDATED` - seen before, fields changed; pushed with `changedFields` listing exactly which fields changed.
- `UNCHANGED` - seen before, nothing changed; not pushed (unless **Also push unchanged rows** is on).
- `EXPIRED` - in the previous run but no longer returned by this search; pushed as a tombstone row with the last known values - but only when the search ran to completion (a run cut short by **Max listings**, **Max pages** or connection trouble never declares hotels gone).

The Output tab has separate tables: **Listings**, **Hotel details** (amenities, policies, nearby places, review aggregates), **Guest reviews** (one table row per review) and **Room offers** (one table row per room); reviews and rooms stay nested inside each hotel record in the dataset. A run with more than one destination or URL also saves an `OUTPUT` record in the key-value store with the hotels delivered per destination or URL.

Every pushed row carries `changeType`, `changedFields`, `firstSeenAt` and `lastSeenAt`, and the dataset's **Changes** output tab shows just the change columns. Two knobs: `ignoreFieldsForChanges` lists dataset fields that never count as a change (they stay in the output, they just don't flip a row to UPDATED), and `emitExpired: false` skips tombstone rows. UPDATED compares the whole record (hotel prices, taxes total, discount, ratings, amenities, policies, room prices and remaining-room counts, requested reviews) and never `scrapedAt`, the image URLs (`images`, `image`) or your ignored fields; the order of lists is ignored. `ignoreFieldsForChanges` also accepts dotted paths into lists (`rooms.roomPromotions`). Optional, all off by default: `changeScope: priceAndStock` makes only hotel price fields and per-room price, name and remaining-room count produce UPDATED; `compactUpdates` slims UPDATED rows to ids, link, current prices, `priceChanges` and `roomChanges` (no detail or review payload, so no detail or review charge on those rows); `expireAfterMissedScans` (default 1) tombstones a hotel only after N consecutive complete scans miss it. EXPIRED means the search stopped returning the hotel, not that it is unavailable. A partial scan (page or listing cap, a pasted `page=N` above 1, a resumed run) never produces EXPIRED rows. If a run is migrated to another server, the restart skips hotels already delivered, so they are not pushed or billed twice. If a run fails mid-way, point **Resume from run ID** at it and the next run continues its dedup set instead of re-scraping.

### Send results into your apps (MCP connectors)

Optionally pipe the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. This is an extra delivery step **after** the scrape: the Apify dataset is never changed.

**What gets written to the connector:** a condensed, human readable **summary** of each record, not the full JSON. Each hotel becomes one entry with a **title** and its key fields flattened to plain text. The **complete record always stays in the Apify dataset**.

1. Authorize a connector once under **Apify -> Settings -> Integrations** (Notion, Linear, Airtable, or Apify).
2. Select it in the **"Pipe results into your apps"** input field. (If the picker is empty, you haven't authorized a connector yet.)
3. For **Notion**, also set `notionParentPageUrl` to the page where items should be created.

The connection is mediated by Apify's MCP proxy, so this actor never sees your third party credentials. Leave the field empty to skip.

### Input Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `mode` | string | `search` | `search` (destination and dates) or `url` (paste Trip.com links). |
| `locations` | array | `["Tokyo"]` | Search mode. One destination per line: a city name ("Tokyo"), a Trip.com city id ("228"), or `"228\|Tokyo"`. |
| `checkIn` | string | 14 days from today | Check in date, `YYYY-MM-DD`. Also the default for pasted list links missing dates. |
| `checkOut` | string | one night after check in | Check out date, `YYYY-MM-DD`. |
| `adults` | integer | `2` | Adults per room. |
| `children` | integer | `0` | Children per room. |
| `rooms` | integer | `1` | Number of rooms. |
| `currency` | string | `USD` | Currency for prices. |
| `minStarRating` | integer | `0` | Keep only hotels rated at or above this many stars, `0` = no filter. Also applied to pasted list links. |
| `minReviewScore` | integer | `0` | Keep only hotels scoring at or above this out of 10, `0` = no filter. Also applied to pasted list links. |
| `urls` | array | one example link per link shape | URL mode. One or more Trip.com hotel links, any shape (see the table above). |
| `fetchDetails` | boolean | `true` | Also fetch each hotel's detail page for amenities, policies, nearby places, description and reviews. |
| `maxReviews` | integer | `20` | Cap the guest reviews attached to each hotel. `0` skips reviews. |
| `sortBy` | string | `mostRelevant` | Order of the reviews on each hotel: `mostRelevant` (site order), `mostRecent`, `ratingHighToLow`, `ratingLowToHigh`. Applied to the reviews the hotel page returns, before the cap. |
| `cutoffDate` | string | empty | Keep only reviews written after this date (`YYYY-MM-DD`). |
| `maxPages` | integer | (none, walk every page) | Max list pages to walk per destination or URL. |
| `maxListings` | integer | `20` | Stop after this many hotels total. `0` = no cap. |
| `resumeFromRunId` | string | (none) | Continue an interrupted run: its hotels join this run's dedup set. |
| `incrementalMode` | boolean | `false` | Remember delivered hotels per search scope and push only NEW/UPDATED rows (plus EXPIRED tombstones), stamped with change metadata. |
| `stateKey` | string | (auto) | Name for the incremental baseline. Empty = keyed automatically on the search scope. |
| `emitUnchanged` | boolean | `false` | With incremental mode, also push rows that did not change. |
| `emitExpired` | boolean | `true` | With incremental mode, push EXPIRED tombstone rows for hotels the search no longer returns. |
| `ignoreFieldsForChanges` | array | (none) | Dataset fields or dotted list paths that never count as a change in incremental mode. |
| `changeScope` | string | `all` | `priceAndStock` limits UPDATED to price and room availability changes. |
| `compactUpdates` | boolean | `false` | Slim UPDATED rows with `priceChanges` / `roomChanges`. |
| `expireAfterMissedScans` | integer | `1` | Consecutive complete scans a hotel must miss before its EXPIRED row. |
| `proxy` | object | Apify Proxy | Connection settings. |
| `mcpConnectors` | array | (none) | Optional: send a summary of each hotel to apps you authorized under Integrations. |
| `notionParentPageUrl` | string | (none) | Notion connector only: page under which records are created. |
| `maxNotifyListings` | integer | `50` | Cap on items written to each connector per run. |

### Output Example

Sample shape, values are illustrative placeholders, not from a live listing.

```json
{
  "hotelId": "0000000",
  "name": "Sample Grand Hotel",
  "starRating": 4,
  "reviewScore": 9.1,
  "reviewScoreLabel": "Great",
  "reviewCount": 1200,
  "reviewTags": ["Easy to get around", "Delicious breakfast"],
  "address": "Sample District, Sample City",
  "lat": 35.0000,
  "lng": 139.0000,
  "priceCurrent": 120,
  "priceOriginal": 140,
  "priceTotalWithTaxes": 134,
  "discountPercent": 14,
  "discountLabel": "14% off",
  "currency": "USD",
  "images": ["https://example.com/photo1.jpg"],
  "image": "https://example.com/photo1.jpg",
  "badges": ["Free Cancellation"],
  "medal": null,
  "propertyTag": "Hotel",
  "detailUrl": "https://www.trip.com/hotels/detail/?hotelId=0000000&checkIn=2026-11-15&checkOut=2026-11-16&adult=2&children=0&curr=USD",
  "amenities": ["Free WiFi", "Fitness center", "Parking"],
  "policies": { "checkin": "After 15:00", "checkout": "Before 11:00", "cancellation": "Free cancellation" },
  "nearbyPlaces": [{ "name": "Sample Station", "type": "station", "distance": "400m" }],
  "contactPhone": "+1-000-0000000",
  "contactEmail": "stay@samplegrandhotel.com",
  "rooms": [
    { "roomId": "0", "roomName": "Superior Queen Room", "roomPrice": { "current": 120, "original": 140, "currency": "USD", "taxesAndFeesTotal": 14 } }
  ],
  "roomCount": 1,
  "reviews": [
    { "reviewId": "0", "reviewerNickname": "Jane Doe", "reviewRatingOverall": 9, "reviewContent": "Great location and clean room." }
  ],
  "reviewCount_pushed": 20,
  "aggregateTotalReviewCount": 1200,
  "aggregateRatingBreakdown": { "overall": 9.1, "location": 9.3, "facility": 8.9, "service": 9.0, "room": 9.0 },
  "aggregateCommentTags": ["Clean and tidy", "Great location"],
  "aggregateAiSummary": "Facilities: guests liked the range of amenities. Guest Suggestion: some guests wanted a quicker check-in process.",
  "cityId": 228,
  "cityName": "Tokyo",
  "scrapedAt": "2026-11-01T00:00:00.000Z"
}
```

> Sample shape: with incremental mode on, rows also carry `changeType` (`NEW` / `UPDATED` / `UNCHANGED` / `REAPPEARED` / `EXPIRED`), `changedFields`, `firstSeenAt` and `lastSeenAt`.

### Plan Requirement

The default proxy setting is a datacenter connection and works on any paid Apify plan; if a page is refused the actor retries on a fresh connection, and only escalates to a capped residential pool when needed. If your Apify plan includes no proxy access, the run logs a warning and continues on a backup connection, and a run that could not load any page ends as failed with an explanatory message rather than succeeding with zero results. Supplying your own proxy URLs uses only those URLs. For large or frequent runs, the residential proxy group gives more headroom; pick it under Connection.

### FAQ

#### How much does it cost?

You're charged once when a run starts, per hotel returned, and a small amount for each hotel enriched from the detail page (rooms, policies, reviews). Both use the existing detail-enrichment event: exactly one unit per enriched hotel that is pushed, charged right after the row is delivered; delivered reviews never add units. The run stops when your maximum charge is reached, whether or not detail pages are fetched, and a hotel the platform could not store at that point is not billed a detail unit. The **Pricing** tab shows the current rates. Use **Max listings** and **Max reviews** to cap the cost of any run.

#### Is it legal to scrape Trip.com?

This actor collects only publicly available hotel listing data. You are responsible for how you use it: follow Trip.com's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution. Prices and availability change constantly and are shown for reference, not as a booking guarantee.

#### Can I schedule this to only get new or changed hotels?

Yes - turn on **Incremental mode**. The actor remembers what it delivered for your search scope and each scheduled run pushes only new and changed hotels (plus `EXPIRED` tombstones for hotels that disappeared), each row stamped with `changeType` and `changedFields`. Without it, each run is an independent search returning the full current result set.

#### Why did I get fewer hotels than I expected from one destination?

Trip.com's hotel list page doesn't expose real page numbers to a plain fetch, so each page is a resampled slice of the destination's hotel pool and depth comes from repeated fetching, not from advancing a page counter. A run also stops a destination once several resamples in a row bring back nothing new, and **Max listings** is split evenly across all your destinations and links. If the same city appears twice in one run (listed twice, two link variants, or the same city with different dates or guests in URL mode), both entries share one dedup set keyed by hotel id: the second only returns hotels the first didn't, can stop after a few hotels, and stops after consecutive samples bring back no new hotels. List each city once and raise **Max listings** or **Max pages** instead, and use one run per date range to compare dates.

#### Why was my destination name not found?

Destination names resolve against a built-in index of Trip.com's own destinations - the major cities everywhere plus every destination Trip.com publishes in its directory. A small town may be missing: use the nearest bigger city, or switch to URL mode and paste any Trip.com hotel link for the town (links copied from a search engine carry its city id and work directly). The run log says exactly this whenever a name can't be resolved.

#### Why did I get an empty dataset instead of an error?

A refused request does not fail the run: every page fetch is retried automatically on a different connection, and if a destination or URL still can't be read after retries, that one entry is skipped with a warning in the log while the rest of the run continues. If a destination simply has no matching hotels, you get an empty dataset with warning lines explaining what to check (the destination name, the date range, or trying URL mode with a link copied from your browser). The exception is a run that could not load a single results page at all: it ends as failed with a message saying so (a connection problem, or on a plan without proxy access, that a paid plan or your own proxy URLs are needed), never as a silent empty success. With incremental mode on, an empty dataset with an informational log line can also mean every hotel was already known and unchanged - use **Also push unchanged rows** if you want the full set every run.

#### Can I use it with AI agents or MCP?

Yes. Call it from any Apify integration or MCP client, and use the connector field to push results into Notion, Linear or Airtable.

### 🔗 Want more travel data?

Pair this actor with these related scrapers from the same team:

<table>
<tr><td>🏨 <a href="https://apify.com/abotapi/expedia-universal-scraper"><b>Expedia Hotels Scraper</b></a><br>Scrape Expedia.com hotel listings and reviews for any destination. Extract prices...</td><td>🏨 <a href="https://apify.com/abotapi/wego-com-scraper"><b>Wego Scraper</b></a><br>Scrape Wego hotels by city or search URL. Get rates, providers, booking links...</td></tr>
<tr><td>🏨 <a href="https://apify.com/abotapi/rakuten-travel-scraper"><b>Rakuten Travel Scraper</b></a><br>Scrape Rakuten Travel, Japan's largest domestic hotel-booking site, by prefecture...</td><td>🏨 <a href="https://apify.com/abotapi/traveloka-com-scraper"><b>Traveloka Hotel Scraper</b></a><br>Scrape Traveloka hotels across Singapore, Malaysia, Indonesia, Thailand, Vietnam and the...</td></tr>
<tr><td>🏨 <a href="https://apify.com/abotapi/hostelworld-scraper"><b>Hostelworld Scraper</b></a><br>Scrape Hostelworld hostels, hotels, guesthouses, B&amp;Bs, apartments and campsites by city...</td><td>🏨 <a href="https://apify.com/abotapi/klook-activities-scraper"><b>Klook Scraper</b></a><br>Scrape Klook activities, tours, attractions and travel experiences from search pages or...</td></tr>
</table>

👉 [Browse all abotapi scrapers](https://apify.com/abotapi)

### 💬 Support

- 🐞 **Found a bug or a missing field?** Open a ticket on the [Issues tab](https://apify.com/abotapi/trip-com-scraper/issues/open). We usually reply within hours.
- 🛠️ **Need another site, extra fields or a private build?** Email <contact@abotapi.com>.
- ⭐ **Enjoying it?** A quick review on the actor page helps other users find it.

# Actor input Schema

## `mode` (type: `string`):

Search: type a destination name, pick dates and filters below. URL: paste trip.com hotel links copied from your browser - every link shape works (search links, hotel detail links, and the /hotels/<city>-hotels-list-<id>/ links copied from a search engine).

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

One destination per line - just the city name, e.g. "Tokyo" or "New York". Trip.com city ids also work: a bare id ("228") or "228|Tokyo". Names are matched against a built-in index of trip.com's own destinations; for smaller towns not in the index, switch to URL mode and paste a trip.com hotel link for that place.

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

Check-in date, format YYYY-MM-DD. Leave empty to default to 14 days from today.

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

Check-out date, format YYYY-MM-DD. Leave empty to default to one night after check-in.

## `adults` (type: `integer`):

Number of adult guests per room.

## `children` (type: `integer`):

Number of child guests per room.

## `rooms` (type: `integer`):

Number of rooms to search for.

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

Currency prices are shown and pushed in.

## `minStarRating` (type: `integer`):

Keep only hotels rated at or above this many stars. 0 = no filter. Applied client-side after fetching each page.

## `minReviewScore` (type: `integer`):

Keep only hotels scoring at or above this out of 10 from guest reviews. 0 = no filter. Applied client-side after fetching each page.

## `urls` (type: `array`):

One or more trip.com hotel URLs, any shape: a hotel list search URL (/hotels/list?city=...), a hotel detail URL (/hotels/detail/?hotelId=...), or the SEO links trip.com and search engines use (/hotels/tokyo-hotels-list-228/, /hotels/tokyo-hotel-detail-123456/hotel-name/, landmark and brand pages). Multi-URL supported, pagination walks forward, missing dates/guests are filled from the fields above. Non-hotel links (homepage, city directories) are skipped with a warning. Dates, guests and currency in a link win over the run-level fields above. Trip.com ignores page, sort and star in a plain list link (each fetch returns a fresh slice of the destination), so use Minimum star rating and Minimum guest review score, which are also applied to hotels from pasted links. A hotelUrl key is accepted as a single-link alias. Left untouched, this field scrapes the example links below, one per link shape; clear it and paste your own to scrape only those.

## `fetchDetails` (type: `boolean`):

When ON, also fetch each hotel's detail page for amenities, policies, nearby places, full description, and guest reviews with an AI summary. When OFF, only the listing-page fields are returned (name, price, rating, image, address). Default ON since most of the field set lives on the detail page.

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

Cap the guest reviews attached to each hotel (the most recent ones trip.com returns on the page). Default 20, 0 to skip reviews. Reviews bill on the existing detail-enrichment event in blocks: one extra unit per 10 delivered reviews (see the Pricing tab).

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

Order of the reviews attached to each hotel, applied to the reviews the page returns, before the cap. mostRelevant keeps the site order.

## `cutoffDate` (type: `string`):

Keep only reviews written after this date (YYYY-MM-DD). Empty keeps all.

## `maxPages` (type: `integer`):

How many list pages to walk per destination (or per pasted list URL). Does not cap the total, only how many pages are fetched, defers to Max listings. Leave empty to walk every result page.

## `maxListings` (type: `integer`):

Stop after this many hotels across all destinations. This is the run's cap, defaulting to 20. Set to 0 to remove the cap entirely (an empty Max pages will then walk every result page).

## `proxy` (type: `object`):

Apify proxy used to fetch pages. The default below works out of the box.

## `resumeFromRunId` (type: `string`):

ID of an interrupted run whose hotels should not be re-scraped: this run continues the previous one's dedup set. Leave empty to start fresh. For recurring diff runs (new/updated only on a schedule) you don't need this - turn on Incremental mode below instead.

## `incrementalMode` (type: `boolean`):

ON: the actor remembers the hotels it delivered for this exact search scope (keyed automatically, or use State key below) and on each scheduled run classifies every hotel as NEW, UPDATED, UNCHANGED or EXPIRED - pushing only new and changed rows plus EXPIRED tombstones, and stamping each row with changeType, changedFields, firstSeenAt and lastSeenAt. UPDATED is decided by comparing the whole record: price, original price, taxes total, discount, rating and review counts, amenities, policies, rooms (room prices and remaining-room counts) and the reviews you requested. It does NOT compare scrapedAt, the image URLs, or any field you list under Ignore fields; list order is ignored. Use Change scope to compare only prices and room availability. OFF (default): every run returns the full result set like before.

## `stateKey` (type: `string`):

Name for the incremental baseline when Incremental mode is ON. Leave empty to key the baseline on a hash of the search scope automatically (recommended). Use the same key across runs to compare like with like; a different key starts a new baseline.

## `emitUnchanged` (type: `boolean`):

With Incremental mode ON, also push hotels that did not change (stamped changeType=UNCHANGED). Default OFF - unchanged hotels are only remembered, not pushed.

## `emitExpired` (type: `boolean`):

With Incremental mode ON, push a tombstone row (changeType=EXPIRED, last known field values) for hotels from the previous run that this run's search no longer returns - only when the search ran to completion. Default ON.

## `changeScope` (type: `string`):

With Incremental mode ON. 'all' (default): any change in the compared fields makes a row UPDATED. 'priceAndStock': only price changes (current, original, total with taxes, discount, currency) and per-room price, room name and remaining-room count make a row UPDATED; descriptions, amenities, badges, reviews and images never do.

## `compactUpdates` (type: `boolean`):

With Incremental mode ON, UPDATED rows carry only ids, name, link, current prices, priceChanges {field: {old, new}} and roomChanges \[{roomId, change: ADDED|REMOVED|CHANGED, changes}] instead of the full record (no detail or review payload, so no detail or review charge rides on a compact row). NEW rows stay full. Default OFF.

## `expireAfterMissedScans` (type: `integer`):

With Incremental mode and Push EXPIRED tombstones ON: a hotel is tombstoned only after it was missing from this many CONSECUTIVE complete scans (default 1 = the first complete scan that misses it). EXPIRED means the search no longer returned the hotel; it does not prove the hotel is unavailable, since a result-pool resample and a delisting look the same without a detail probe.

## `ignoreFieldsForChanges` (type: `array`):

With Incremental mode ON, dataset fields listed here never count as a change (they are excluded from changeType/changedFields while staying in the output). Top-level names (e.g. badges) or dotted paths into a list (e.g. rooms.roomPromotions drops that key from every room, the rest of the room is still compared). Built-in defaults already cover the scrape timestamp and the image URLs (images, image), and lists whose order the site reshuffles are compared order-independently.

## `mcpConnectors` (type: `array`):

Optionally send the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. Authorize a connector once under Apify -> Settings -> Integrations, then select it here. The connector receives a condensed, human-readable summary per item (title + key fields), not the full JSON, the complete record stays in the dataset. Leave empty to skip. Supported: Notion (https://mcp.notion.com/mcp), Linear (https://mcp.linear.app/sse), Airtable (https://mcp.airtable.com/mcp), Apify (https://mcp.apify.com).

## `notionParentPageUrl` (type: `string`):

URL (or id) of the Notion page under which item pages are created. Required to enable the Notion export, ignored by other connectors.

## `maxNotifyListings` (type: `integer`):

Cap on items written to each connector per run. Does not affect the dataset.

## Actor input object example

```json
{
  "mode": "search",
  "locations": [
    "Tokyo"
  ],
  "adults": 2,
  "children": 0,
  "rooms": 1,
  "currency": "USD",
  "minStarRating": 0,
  "minReviewScore": 0,
  "urls": [
    "https://www.trip.com/hotels/list?city=228&cityName=Tokyo&checkIn=2026-11-15&checkOut=2026-11-16&adult=2&children=0&crn=1&curr=USD",
    "https://us.trip.com/hotels/list?city=359",
    "https://www.trip.com/hotels/tokyo-hotels-list-228/",
    "https://au.trip.com/hotels/singapore-hotels-list-73/",
    "https://www.trip.com/hotels/tokyo-asakusa/hotels-c228m15019878/",
    "https://www.trip.com/hotels/shanghai-ibis/hotels-c2b1137/",
    "https://www.trip.com/hotels/tokyo-jr-tokyo-station/hotels-c228r8254862/",
    "https://www.trip.com/hotels/detail/?hotelId=1677831",
    "https://www.trip.com/hotels/detail/?hotelId=686797&checkIn=2026-11-15&checkOut=2026-11-16&adult=2&crn=1&curr=USD",
    "https://www.trip.com/hotels/tokyo-hotel-detail-993510/imperial-hotel-tokyo/",
    "https://www.trip.com/hotels/tokyo-hotel-detail-1489108/",
    "https://www.trip.com/hotels/dali-hotel-detail-21785314/liu-minyue-garden-designer-vacation-accommodation/review.html",
    "https://www.trip.com/hotels/ho-chi-minh-city-hotel-detail-2073112/park-hyatt-saigon/photo.html"
  ],
  "fetchDetails": true,
  "maxReviews": 20,
  "sortBy": "mostRelevant",
  "maxListings": 20,
  "proxy": {
    "useApifyProxy": true
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": true,
  "changeScope": "all",
  "compactUpdates": false,
  "expireAfterMissedScans": 1,
  "ignoreFieldsForChanges": [],
  "maxNotifyListings": 50
}
```

# Actor output Schema

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

No description

## `detail` (type: `string`):

No description

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

No description

## `rooms` (type: `string`):

No description

## `changes` (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 = {
    "mode": "search",
    "locations": [
        "Tokyo"
    ],
    "urls": [
        "https://www.trip.com/hotels/list?city=228&cityName=Tokyo&checkIn=2026-11-15&checkOut=2026-11-16&adult=2&children=0&crn=1&curr=USD",
        "https://us.trip.com/hotels/list?city=359",
        "https://www.trip.com/hotels/tokyo-hotels-list-228/",
        "https://au.trip.com/hotels/singapore-hotels-list-73/",
        "https://www.trip.com/hotels/tokyo-asakusa/hotels-c228m15019878/",
        "https://www.trip.com/hotels/shanghai-ibis/hotels-c2b1137/",
        "https://www.trip.com/hotels/tokyo-jr-tokyo-station/hotels-c228r8254862/",
        "https://www.trip.com/hotels/detail/?hotelId=1677831",
        "https://www.trip.com/hotels/detail/?hotelId=686797&checkIn=2026-11-15&checkOut=2026-11-16&adult=2&crn=1&curr=USD",
        "https://www.trip.com/hotels/tokyo-hotel-detail-993510/imperial-hotel-tokyo/",
        "https://www.trip.com/hotels/tokyo-hotel-detail-1489108/",
        "https://www.trip.com/hotels/dali-hotel-detail-21785314/liu-minyue-garden-designer-vacation-accommodation/review.html",
        "https://www.trip.com/hotels/ho-chi-minh-city-hotel-detail-2073112/park-hyatt-saigon/photo.html"
    ],
    "proxy": {
        "useApifyProxy": true
    },
    "ignoreFieldsForChanges": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/trip-com-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 = {
    "mode": "search",
    "locations": ["Tokyo"],
    "urls": [
        "https://www.trip.com/hotels/list?city=228&cityName=Tokyo&checkIn=2026-11-15&checkOut=2026-11-16&adult=2&children=0&crn=1&curr=USD",
        "https://us.trip.com/hotels/list?city=359",
        "https://www.trip.com/hotels/tokyo-hotels-list-228/",
        "https://au.trip.com/hotels/singapore-hotels-list-73/",
        "https://www.trip.com/hotels/tokyo-asakusa/hotels-c228m15019878/",
        "https://www.trip.com/hotels/shanghai-ibis/hotels-c2b1137/",
        "https://www.trip.com/hotels/tokyo-jr-tokyo-station/hotels-c228r8254862/",
        "https://www.trip.com/hotels/detail/?hotelId=1677831",
        "https://www.trip.com/hotels/detail/?hotelId=686797&checkIn=2026-11-15&checkOut=2026-11-16&adult=2&crn=1&curr=USD",
        "https://www.trip.com/hotels/tokyo-hotel-detail-993510/imperial-hotel-tokyo/",
        "https://www.trip.com/hotels/tokyo-hotel-detail-1489108/",
        "https://www.trip.com/hotels/dali-hotel-detail-21785314/liu-minyue-garden-designer-vacation-accommodation/review.html",
        "https://www.trip.com/hotels/ho-chi-minh-city-hotel-detail-2073112/park-hyatt-saigon/photo.html",
    ],
    "proxy": { "useApifyProxy": True },
    "ignoreFieldsForChanges": [],
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/trip-com-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 '{
  "mode": "search",
  "locations": [
    "Tokyo"
  ],
  "urls": [
    "https://www.trip.com/hotels/list?city=228&cityName=Tokyo&checkIn=2026-11-15&checkOut=2026-11-16&adult=2&children=0&crn=1&curr=USD",
    "https://us.trip.com/hotels/list?city=359",
    "https://www.trip.com/hotels/tokyo-hotels-list-228/",
    "https://au.trip.com/hotels/singapore-hotels-list-73/",
    "https://www.trip.com/hotels/tokyo-asakusa/hotels-c228m15019878/",
    "https://www.trip.com/hotels/shanghai-ibis/hotels-c2b1137/",
    "https://www.trip.com/hotels/tokyo-jr-tokyo-station/hotels-c228r8254862/",
    "https://www.trip.com/hotels/detail/?hotelId=1677831",
    "https://www.trip.com/hotels/detail/?hotelId=686797&checkIn=2026-11-15&checkOut=2026-11-16&adult=2&crn=1&curr=USD",
    "https://www.trip.com/hotels/tokyo-hotel-detail-993510/imperial-hotel-tokyo/",
    "https://www.trip.com/hotels/tokyo-hotel-detail-1489108/",
    "https://www.trip.com/hotels/dali-hotel-detail-21785314/liu-minyue-garden-designer-vacation-accommodation/review.html",
    "https://www.trip.com/hotels/ho-chi-minh-city-hotel-detail-2073112/park-hyatt-saigon/photo.html"
  ],
  "proxy": {
    "useApifyProxy": true
  },
  "ignoreFieldsForChanges": []
}' |
apify call abotapi/trip-com-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,abotapi/trip-com-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/lFVpkDhftz7ubBhjv/builds/3zaagLkxOlO9mZQ2y/openapi.json
