# Traveloka Hotel Scraper - SEA Hotel Prices & Ratings (`abotapi/traveloka-com-scraper`) Actor

Scrape Traveloka hotels across Singapore, Malaysia, Indonesia, Thailand, Vietnam and the Philippines: nightly prices, star ratings, guest scores, coordinates and photos by city. Paste search or hotel links, and monitor price changes with recurring updates.

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

## Pricing

from $1.80 / 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

## Traveloka Scraper: Hotel Prices, Ratings & Availability Across Southeast Asia

Traveloka Scraper turns Traveloka, a major Southeast Asian travel platform, into structured hotel data. Search any city or area across its Singapore, Malaysia, Indonesia, Thailand, Vietnam, Philippines and Australia storefronts, or paste Traveloka search and hotel links, and get nightly prices in the local currency, star ratings, guest review scores, coordinates, photos and facilities for every hotel. Turn on recurring monitoring to track price and rating changes over time, then export to JSON, CSV or Excel, or pull results straight into your app through the API.

### Why This Scraper?

- **30+ fields per hotel:** nightly price, star rating, guest score out of 10, review count, region, coordinates, photos and facilities.
- **Seven storefronts** (Singapore, Malaysia, Indonesia, Thailand, Vietnam, Philippines, Australia) with per-market currency handling.
- **Two ways to find hotels:** keyword search by city or area, or paste Traveloka search links (with `spec=`) and hotel detail links.
- **Optional enrichment, priced per hotel:** facility codes and labels, feature highlights, extra photos and a backfilled guest rating merged straight from the site's own data feed, no extra page loads for search results. A separate guest-review pull is also available.
- **Recurring updates:** incremental mode classifies every hotel NEW, UPDATED, UNCHANGED or EXPIRED between scheduled runs, so you only pay for what changed.
- **Client-side filters and sorting:** price range, minimum star rating and minimum guest score, plus popularity or price-based sort order.
- **Fail-loud runs:** when the site refuses to serve, the run says so instead of returning an empty result set.

### Use Cases

- **Travel meta-search and price-comparison sites:** pull nightly Traveloka rates across cities to power search widgets or price-alert tools.
- **Hotel revenue teams and market researchers:** track competitor pricing, star ratings and guest scores across Traveloka's storefronts.
- **OTA and travel-tech platforms:** feed structured hotel inventory (facilities, photos, coordinates) into an internal booking or aggregation product.
- **Travel content and city-guide sites:** build listing pages from ratings, facilities and photos without manual data entry.
- **Recurring rate monitoring:** schedule the actor against a fixed set of cities or hotels to catch price and rating changes over time.

### Data You Get

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

| Field | Example |
| --- | --- |
| `hotelId` | `"9034521"` |
| `name` | `"Sample City Hotel"` |
| `url` | `"https://www.traveloka.com/en-sg/hotel/singapore/sample-city-hotel-9034521?checkIn=2026-11-01&checkOut=2026-11-02"` |
| `locationName` | `"Singapore"` (the search location this row was found under) |
| `region` | `"Sample District, Singapore"` |
| `address` | `"1 Sample Street, Singapore"` (empty unless backfilled, see below) |
| `price` | `185.0` |
| `priceMinor` | `18500` (the site's raw integer rate, before the currency's decimal places) |
| `currency` | `"SGD"` |
| `starRating` | `4.0` |
| `guestRating` / `guestRatingMax` | `8.2` / `10.0` |
| `guestRatingLabel` | `"Very Good"` |
| `numReviews` | `1240` |
| `latitude` / `longitude` | `1.301` / `103.855` |
| `facilities` / `facilityCodes` | `["Wi-Fi in public areas", "Swimming pool"]` / `["WIFI_PUBLIC_AREA", "POOL"]` |
| `imageUrl` / `imageUrls` | `"https://ik.imagekit.io/tvlk/sample/asset/sample-hotel-0000000.jpeg"` / up to 8-12 photo URLs |
| `market` | `"en-sg"` |
| `checkInDate` / `checkOutDate` | `"2026-11-01"` / `"2026-11-02"` |
| `changeType` / `changedFields` | `"NEW"` / `[]` on every row; with incremental mode also `"UPDATED"` / `["price"]`, `"UNCHANGED"` or `"EXPIRED"` |
| `firstSeenAt` / `lastSeenAt` | `"2026-11-01T00:00:00Z"` / `"2026-11-01T00:00:00Z"` (carried over between runs in incremental mode) |
| `features` | `["Free cancellation"]` (detail toggle, merged from the site's own data feed) |
| `description` | full text hotel description (detail toggle, pasted hotel detail links only) |
| `amenities` | `["Free Wi-Fi", "Swimming pool"]` (detail toggle, pasted hotel detail links only) |
| `detailFetched` | `true` when `fetchDetails` merged new data from the site's feed (search rows) or read the hotel page (pasted detail links); not set by an address backfill or `fetchReviews` on their own |
| `reviews` | `[{"author": "Guest Name", "rating": 9.0, "ratingMax": 10.0, "date": "2026-09-01", "text": "...", "provider": "GOOGLE"}]` (review toggle) |

Records from a pasted hotel detail link (URL mode) carry `hotelId`, `url`, `currency`, `market`, `checkInDate`, `checkOutDate`, plus (with `fetchDetails` on) the `name`, `description`, `address` and `amenities` the page itself carried, and the page's own `guestRating` / `numReviews` when it publishes them; `locationName`, `region` and `price` are not available for that path since no search feed was read. A hotel link whose page could not be read (with `fetchDetails` on), or whose reviews could not be read (with `fetchReviews` on and `fetchDetails` off, including when the device token is unavailable), is skipped with a warning: it is not saved, not billed and not added to the incremental baseline. A hotel with no reviews at all is still returned. With `fetchReviews` on and `fetchDetails` off, a hotel link carries its reviews but no name, description, address or amenities. With both `fetchDetails` and `fetchReviews` off, a hotel link returns only its id, link, dates and market.

### How to Use

1. Pick a **mode**: `search` (by city or area) or `url` (paste Traveloka links).
2. For search mode, set the **market** (storefront), one or more **locations**, dates and any filters; for URL mode, paste your links (search filters are ignored). If you switch to URL mode and leave the links field untouched, it scrapes two built-in examples (one Singapore search link and one hotel detail link). Search links with a past check-in date are rolled forward to tomorrow, keeping the stay length. Hotel landing pages and activity/experience links are not supported and are skipped with a warning in the log.
3. Turn on detail enrichment or guest reviews if you need them (both add a per-hotel surcharge), and set **Max results** or **Max pages** to control run size and cost.
4. Click **Start**. Download the dataset as JSON, CSV or Excel, or read it through the API.

**Search hotels in one city:**

```json
{
  "mode": "search",
  "market": "en-sg",
  "locations": ["Singapore"],
  "maxItems": 20
}
```

**Search with filters, sorting and multiple cities:**

```json
{
  "mode": "search",
  "market": "en-th",
  "locations": ["Bangkok", "Phuket"],
  "checkInDate": "2026-11-01",
  "checkOutDate": "2026-11-03",
  "adults": 2,
  "sortBy": "price-asc",
  "minStarRating": 4,
  "maxPrice": 5000,
  "maxItems": 40
}
```

**Walk a pasted Traveloka search link and enrich the results:**

```json
{
  "mode": "url",
  "urls": ["https://www.traveloka.com/en-sg/hotel/search?spec=01-12-2026.02-12-2026.1.1.HOTEL_GEO.107493.Singapore.2"],
  "fetchDetails": true,
  "maxItems": 30
}
```

**Monitor a scope for changes (recurring schedule):**

```json
{
  "mode": "search",
  "market": "en-my",
  "locations": ["Kuala Lumpur"],
  "checkInDate": "2026-12-20",
  "checkOutDate": "2026-12-22",
  "maxItems": 50,
  "incrementalMode": true,
  "stateKey": "kl-december-stay",
  "emitUnchanged": false
}
```

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/traveloka-com-scraper").call(run_input={
    "mode": "search", "market": "en-sg", "locations": ["Singapore"], "maxItems": 20,
})
for hotel in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(hotel["name"], hotel["price"], hotel["currency"])
```

JavaScript:

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

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

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

#### Supported link shapes (URL mode)

| Link | Result |
| --- | --- |
| Search link with `spec=` (`/hotel/search?spec=...`) | Walked page by page, one row per hotel |
| Hotel detail link (`/hotel/<city>/<name>-<id>`) | One row for that hotel, read from its page (`fetchDetails` reads its page; `fetchReviews` adds its reviews, with or without `fetchDetails`) |
| Hotel landing page without `spec=` | Skipped with a warning: run a search on the site and paste that link |
| Activity or experience link | Skipped with a warning: hotel links only |

When the links field is left untouched in URL mode, the `urls` default is used: one Singapore search link and one hotel detail link, so a first run needs no setup. Search links with a past check-in date are rolled forward to tomorrow, keeping the stay length. Supported markets: en-sg, en-my, en-id, en-th, en-vn, en-ph, en-au.

#### Caps across multiple locations

**Max results** (`maxItems`) is the one real cap for the whole run (up to 100000 hotels, 0 meaning no limit); with several locations or links it is split evenly across each of them, and a location that has fewer hotels than its share does not pass the leftover on to the others. **Max pages** (`maxPages`) separately caps how many result pages are walked per location, regardless of `maxItems`.

Tip: the actor deduplicates hotels once per run by hotel id, so the same hotel is never pushed twice even if two locations or links overlap. In a run with tight per-location budgets, a hotel that is read right as one location's share of the budget runs out is marked as already seen for the rest of the run, even though it was not actually saved to the dataset. If that same hotel also appears under a later, still-under-budget location, it will not be re-tried there either, which can leave a run under its `maxItems` target when locations overlap. Raising `maxItems`, raising `maxPages`, or running overlapping locations as separate runs avoids it.

#### Resume and recurring updates

**Incremental mode** (`incrementalMode`) is for scheduled runs over the same scope. Each hotel is classified `NEW`, `UPDATED` (with `changedFields`), `UNCHANGED` (suppressed and not billed unless `emitUnchanged` is on) or `EXPIRED` (only after a run that scanned the entire scope without hitting a cap, a page limit or a refusal, and only with `emitExpired`). `stateKey` names or shares the stored baseline. `ignoreFieldsForChanges` lists fields whose changes are not counted; changing the list never re-bills the saved baseline. With incremental mode off, every row is `NEW`.

Keep in mind for scheduled monitoring:

- The automatic baseline is tied to your exact input, including the dates. If you leave the dates empty, they roll forward every day, so each day starts a fresh baseline and every hotel comes back `NEW`. Set fixed `checkInDate` / `checkOutDate` for the stay you are watching, or set a `stateKey`.
- With a shared `stateKey` but changing dates, every hotel comes back `UPDATED` with `url` in `changedFields`, because the hotel link carries the dates.
- An `EXPIRED` hotel is emitted (and billed) once, then removed from the baseline, so later complete runs do not emit it again. If it comes back, it is classified `NEW`.

**Resume** (`resumeFromRunId`) continues one interrupted run by skipping hotels it already saved, so they are not fetched or billed again. Do not combine it with Incremental mode.

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

You can optionally send results into the apps you already use through Apify MCP connectors: authorize a connector under Apify, Settings, API and Integrations, then pick it in the `mcpConnectors` input. Notion receives one page per hotel under the page you set in `notionParentPageUrl`; other connectors receive a condensed summary per item (title plus the main fields flattened as text). The connector write is a side channel: it never changes the dataset, and the complete record always stays in the Apify dataset. Cap the export per run with `maxNotifyListings`.

Known issue in the current build: the connector export step fails before any item is sent, and the run only logs "MCP connector export skipped" without failing. Your dataset is complete and unaffected, but connectors currently receive nothing from this actor. Until this is fixed, connect the dataset through the **Integrations** tab instead.

### Input Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `mode` | select | `search` | Search by city or area, or paste Traveloka links. |
| `market` | select | `en-sg` | Storefront locale: sets the language and the currency prices are returned in. |
| `locations` | stringList | `["Singapore"]` | Cities or areas to walk (search mode). Each is resolved to Traveloka's internal region id; multiple locations split the result budget evenly. |
| `checkInDate` | date | tomorrow | Check-in date (YYYY-MM-DD). Leave empty to default to tomorrow (site time). |
| `checkOutDate` | date | day after check-in | Check-out date (YYYY-MM-DD). Leave empty to default to the day after check-in. |
| `adults` | integer | `2` | Number of adult guests per search. |
| `rooms` | integer | `1` | Number of rooms per search. |
| `sortBy` | select | `popularity` | Search-mode sort order: popularity (site default), price low to high, or price high to low. |
| `minPrice` | integer | empty | Drop hotels whose nightly price is below this. Applied to returned results (client side), in the market's currency. |
| `maxPrice` | integer | empty | Drop hotels whose nightly price is above this. Applied to returned results (client side), in the market's currency. |
| `minStarRating` | integer | `0` | Drop hotels below this star rating (0 keeps all). Applied to returned results (client side). |
| `minGuestRating` | number | empty | Drop hotels whose guest review score is below this, out of 10. Applied to returned results (client side); hotels without a score are kept. |
| `urls` | array | sample | Traveloka hotel-search links (with `spec=`) and/or hotel detail links (URL mode). Search filters are ignored in URL mode. |
| `fetchDetails` | boolean | `false` | Merges facility codes and labels, feature highlights, extra photos and a backfilled guest rating into each row. Bills the detail-enrichment surcharge once per enriched row; unenriched rows bill nothing extra. Pasted hotel detail links are read as full pages. |
| `fetchReviews` | boolean | `false` | Adds a `reviews` array (author, rating, date, text) per hotel, up to `maxReviews`. Bills the detail-enrichment surcharge like other enrichment. Works with `fetchDetails` on or off. |
| `reviewSortBy` | string | `DEFAULT` | Order of the reviews array: site default, newest, oldest, most helpful, highest or lowest rating. |
| `reviewCutoffDate` | string | empty | Keep only reviews dated after this day. |
| `aggregateRatings` | boolean | `true` | With `fetchReviews` on, adds an `aggregatedReview` object (`count`, `overallRating`) to each hotel row. Search results take both from the listing; a hotel detail link takes them from the page when it publishes a rating, which requires `fetchDetails` (without it the link carries `reviews` but no `aggregatedReview`). The data this actor reads carries no per-score breakdown, so there is no `distributions`. |
| `maxReviews` | integer | `10` | Cap on review rows per hotel when `fetchReviews` is on. For search results, each enriched row bills detail-enrichment units equal to the number of started blocks of 10 reviews, minimum 1 (10 reviews = 1, 25 = 3, 1000 = 100). A hotel detail link bills 1 unit per row. |
| `maxItems` | integer | `20` | Maximum hotel records to return per run, across all locations. `0` means unlimited. |
| `maxPages` | integer | `0` | Safety cap on result pages walked per location. `0` means no page cap. |
| `resumeFromRunId` | string | empty | ID of an interrupted run to continue, skipping hotels already saved by that run. Conflicts with Incremental mode (pick one). |
| `incrementalMode` | boolean | `false` | Recurring monitoring: only new or changed hotels are emitted on repeated runs of the same scope. Conflicts with Resume from run ID. |
| `stateKey` | string | empty | Name for the storage the incremental baseline is kept under. Leave empty to derive it from your scope automatically. |
| `emitUnchanged` | boolean | `false` | With Incremental mode: also emit (and bill) rows whose fingerprint did not change. |
| `emitExpired` | boolean | `false` | With Incremental mode: also emit (and bill) rows that were tracked before but did not appear in a complete walk. |
| `ignoreFieldsForChanges` | array | `[]` | With Incremental mode: field names that never make a hotel count as updated (use dots for nested fields). They stay in the output. The cover image link and the order of facilities are always ignored. |
| `proxy` | proxy | RESIDENTIAL (Apify Proxy) | With the default RESIDENTIAL selection the run starts on Apify residential (a multi-country pool) and fails over to backup connections when an exit is refused. Only if you remove the RESIDENTIAL group does it start on a datacenter lane and escalate to residential; see Plan Requirement. |
| `mcpConnectors` | array | empty | Optionally send results into the apps you already use through Model Context Protocol connectors, authorized under Apify Settings, API and Integrations. |
| `notionParentPageUrl` | string | empty | Notion connector only: the page under which item pages are created. |
| `maxNotifyListings` | integer | `50` | Cap on items written to each connector per run. Does not affect the dataset. |

### Output Example

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

```json
{
  "hotelId": "9034521",
  "name": "Sample City Hotel",
  "url": "https://www.traveloka.com/en-sg/hotel/singapore/sample-city-hotel-9034521?checkIn=2026-11-01&checkOut=2026-11-02",
  "locationName": "Singapore",
  "region": "Sample District, Singapore",
  "address": null,
  "price": 185.0,
  "priceMinor": 18500,
  "currency": "SGD",
  "starRating": 4.0,
  "guestRating": 8.2,
  "guestRatingMax": 10.0,
  "guestRatingLabel": "Very Good",
  "numReviews": 1240,
  "latitude": 1.301,
  "longitude": 103.855,
  "facilities": ["Wi-Fi in public areas"],
  "facilityCodes": ["WIFI_PUBLIC_AREA"],
  "imageUrl": "https://ik.imagekit.io/tvlk/sample/asset/sample-hotel-0000000.jpeg",
  "imageUrls": ["https://ik.imagekit.io/tvlk/sample/asset/sample-hotel-0000000.jpeg"],
  "market": "en-sg",
  "checkInDate": "2026-11-01",
  "checkOutDate": "2026-11-02",
  "changeType": "NEW",
  "changedFields": [],
  "firstSeenAt": "2026-11-01T00:00:00Z",
  "lastSeenAt": "2026-11-01T00:00:00Z"
}
```

With `fetchDetails` on, search-mode rows also carry `features`, extra `facilityCodes`/`facilities`/`imageUrls` and a backfilled `guestRating` merged from the site's own data feed, plus `detailFetched`; a backfilled `address` is added when coordinates are available. Pasted hotel detail links additionally carry `description` and `amenities` read from the page itself. With `fetchReviews` on, rows also carry a `reviews` array (each review adds reviewer name, timestamp, score, original text, reaction count, provider and any host reply fields) and an `aggregatedReview` summary. The dataset has four views: Overview, Reviews (one row per review), Hotel details and Changes (change type and changed fields for incremental runs). Each run also stores a summary in the key-value store under `OUTPUT`.

**Reviews:** `fetchReviews` adds up to `maxReviews` reviews per hotel (default 10), ordered by `reviewSortBy` and limited to dates after `reviewCutoffDate`; `aggregateRatings` adds the `aggregatedReview` summary (`count` and `overallRating`). The order and the date filter are applied to the reviews the actor reads in the site's own order, so "newest first" orders those reviews and does not search the hotel's whole review list. Each hotel's review read has a time limit; if it runs out, the reviews read so far are kept and a warning is logged.

### Plan Requirement

Traveloka sits behind a bot-verification wall that plain datacenter or direct requests do not clear, so a paid Apify plan with proxy access is required. With the default RESIDENTIAL proxy selection the actor starts on Apify residential and then tries the backup connections, and it keeps using the one that was served. If you remove the RESIDENTIAL group it starts on a datacenter lane instead (which Traveloka usually refuses), escalates to residential, then to the backup connections. On a free plan, which has no residential proxy, the run logs: "The selected connection is unavailable to this run. Apify free plans do not include residential proxies. Continuing on a backup connection; some pages may still be unavailable." Pages served that way log "Served over the backup connection (the default connection was unavailable)." You can also supply your own proxy URLs.

### FAQ

#### How much does it cost?

You pay per hotel record returned (the dataset-item event), plus a small per-run start event. Detail enrichment and guest reviews use the detail-enrichment event, charged once per enriched row. For search results the units per row equal the number of started blocks of 10 reviews, minimum 1; a hotel detail link bills 1 unit per row whatever its review count. Unchanged, duplicate and failed rows are not charged: a hotel link whose page or reviews could not be read is skipped, not saved and not billed. The **Pricing** tab shows the current rates. Use **Max results** to cap the cost of any run.

#### Is it legal to scrape Traveloka?

This actor collects only publicly available hotel listing data. You are responsible for how you use it: follow Traveloka'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 not guaranteed to be bookable at the time you read them.

#### Can I get only new or changed hotels on a schedule?

Yes. Schedule the actor from the **Schedules** tab and turn on **Incremental mode**. Each run then returns only new and updated records, and unchanged ones are not billed unless you turn on **Emit unchanged rows**. Use fixed check-in and check-out dates (or a **State key**) for the scope you are watching: with empty dates the scope changes every day and every hotel comes back as new.

#### Why did my price or rating filters return fewer hotels than Max results?

`minPrice`, `maxPrice`, `minStarRating` and `minGuestRating` are applied after each page is fetched (client side), not sent to the site as a request filter. A page can be mostly filtered out and still count toward the page budget, so a heavily filtered search may need a higher **Max results** or **Max pages** to fill your target count.

#### Why did my run fail instead of returning an empty dataset?

A run that could not read any page stops with one of these messages, so "no hotels for these dates" is never confused with "nothing could be read":

- "The proxy connection was refused for this account, so no page could be read. Your Apify plan does not include proxy access; a paid plan is required." Upgrade, or check your own proxy credentials.
- "No results: residential proxies are unavailable on this run (Apify free plans do not include them), and the backup connection could not load the requested Traveloka pages." Upgrade or supply your own proxy URLs.
- "Could not load any results page (the requests failed), so nothing was collected. This is a connection problem, not an empty search." Try again later.

If only part of the walk was refused, you still get what was found, and the run reports the shortfall in its status message.

#### Does Resume from run ID actually skip hotels from the earlier run?

Yes. Pass the interrupted run's ID in `resumeFromRunId` and hotels that run already saved are skipped, so they are not fetched or billed again. It cannot be combined with Incremental mode.

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

Yes. Any MCP client or AI agent that can call Apify actors can run it and read the dataset. The built-in connector field (`mcpConnectors`) is meant to push results into Notion, Linear or Airtable, but in the current build that export sends nothing (see the MCP connectors section), so read the dataset directly for now.

### 🔗 Want more travel data?

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

<table>
<tr><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><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></tr>
<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/trip-com-scraper"><b>Trip.com Hotels, Rooms &amp; Reviews Scraper</b></a><br>Pull structured hotel listings from trip.com with prices, room types, amenities...</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/airbnb-stays-reviews-scraper"><b>Airbnb Scraper</b></a><br>Scrape Airbnb stays by destination or link: nightly and total price, rating breakdown...</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/traveloka-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 walks hotel results for the locations below. URL mode pastes Traveloka hotel-search links (with spec=) or hotel detail links; search filters are ignored in URL mode.

## `market` (type: `string`):

Which Traveloka storefront to read. Sets the language and the currency prices are returned in.

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

City or area names as you would type them into Traveloka's hotel search box (e.g. Singapore, Bangkok, Kuta). Each location is resolved to Traveloka's internal region id, then its results are walked. Multiple locations are split evenly across your result budget.

## `checkInDate` (type: `string`):

Check-in date (YYYY-MM-DD). Leave empty to default to tomorrow (site time).

## `checkOutDate` (type: `string`):

Check-out date (YYYY-MM-DD). Leave empty to default to the day after check-in.

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

Number of adult guests per search.

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

Number of rooms per search.

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

Search-mode sort order requested from the site. Popularity is Traveloka's own default order.

## `minPrice` (type: `integer`):

Drop hotels whose nightly price is below this. Applied to returned results (client side), in the market's currency.

## `maxPrice` (type: `integer`):

Drop hotels whose nightly price is above this. Applied to returned results (client side), in the market's currency.

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

Drop hotels below this star rating (0 = keep all). Applied to returned results (client side).

## `minGuestRating` (type: `number`):

Drop hotels whose guest review score is below this (Traveloka scores out of 10). Applied to returned results (client side); hotels without a score are kept.

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

Multi-URL supported. Paste hotel-search links that contain spec= (from a browser search) and/or hotel detail links. Filter-mode fields are ignored; a search link walks forward from its own dates and occupancy.

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

Merges the static-content feed into each row: facility codes + labels, feature highlights, extra images and UGC ratings. Each enriched row bills the detail-enrichment surcharge event once; unenriched rows push the base listing and bill nothing extra. A pasted hotel detail link is read as a page only when this is on; a link whose page cannot be read is skipped and not billed. With only Read guest reviews on, a link whose reviews cannot be read is skipped and not billed too.

## `fetchReviews` (type: `boolean`):

Adds a reviews array (author, rating, date, text) from the review JSON lane, up to maxReviews per hotel. Rows with fetched reviews bill the detail-enrichment surcharge like other enrichment (search results: started blocks of 10 reviews, minimum 1; hotel detail links: 1 unit per row). Works with fetchDetails on or off.

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

Cap on review rows per hotel when fetchReviews is on. Billing scales with volume: for search results each enriched row bills detail-enrichment units equal to the number of started blocks of 10 reviews, minimum 1 (10 reviews = 1, 25 = 3, 1000 = 100). A hotel detail link bills 1 unit per row.

## `reviewSortBy` (type: `string`):

Order of the reviews array when fetchReviews is on. The order is applied to the reviews read for each hotel (up to maxReviews); it does not search the site's whole review list for the best or newest ones.

## `reviewCutoffDate` (type: `string`):

When fetchReviews is on, keep only reviews dated after this day (YYYY-MM-DD). Leave empty for no date filter.

## `aggregateRatings` (type: `boolean`):

When fetchReviews is on, adds an aggregatedReview object (count, overallRating) to each hotel row. The data this actor reads carries no per-score breakdown.

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

The one real cap: maximum hotel records to return per run, across all locations. 0 means unlimited.

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

Safety cap on result pages walked per location. 0 (default) means no page cap: the walk follows Max results and the site's own result count.

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

With the default RESIDENTIAL group the run starts on Apify residential with a multi-country pool (SG/ID/MY/TH/VN/PH, a fresh exit per attempt) and fails over to backup connections when every exit is refused. If you remove the RESIDENTIAL group the run starts on a datacenter lane, which the site usually refuses, and escalates to residential and then the backup connections. Free plans have no residential proxies: bring your own proxy URLs.

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

ID of an interrupted run of this Actor to continue, skipping hotels already saved by that run. Leave empty for a fresh run. Conflicts with Incremental mode (pick one).

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

Recurring monitoring: the Actor remembers hotels it saw on the previous run of the same scope and only emits new or changed ones. Rows carry changeType/changedFields. Unchanged rows are suppressed (set emitUnchanged to also emit and bill them). Conflicts with Resume from run ID.

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

Name for the storage the incremental baseline is kept under. Leave empty to derive it from your scope (mode, market, locations, dates, occupancy and filters), so different setups never share a baseline.

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

With Incremental mode: also emit (and bill) rows whose fingerprint did not change, marked changeType UNCHANGED.

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

With Incremental mode: also emit (and bill) rows that were tracked before but no longer appeared in a complete walk, marked changeType EXPIRED. Never fires when the walk was capped, truncated or refused part-way.

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

Incremental mode only: field names that never make a hotel count as updated, for example price or guestRating. Use dots for nested fields. The fields are still kept in the output. The cover image link is always ignored because its address rotates between runs; hotel facilities are compared regardless of list order. Changing this list never re-bills the saved baseline.

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

Optionally send results into the apps you already use, via Model Context Protocol (MCP) connectors. Authorize one under Apify → Settings → API & Integrations, then select it here. Notion gets a rich page-per-item export; other connectors get a best-effort write/digest. Leave empty to skip (never changes the dataset output). 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",
  "market": "en-sg",
  "locations": [
    "Singapore"
  ],
  "adults": 2,
  "rooms": 1,
  "sortBy": "popularity",
  "minStarRating": 0,
  "urls": [
    "https://www.traveloka.com/en-sg/hotel/search?spec=01-12-2026.02-12-2026.1.1.HOTEL_GEO.107493.Singapore.2",
    "https://www.traveloka.com/en-sg/hotel/singapore/v-hotel-lavender-254056"
  ],
  "fetchDetails": false,
  "fetchReviews": false,
  "maxReviews": 10,
  "reviewSortBy": "DEFAULT",
  "aggregateRatings": true,
  "maxItems": 20,
  "maxPages": 0,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "ignoreFieldsForChanges": [],
  "maxNotifyListings": 50
}
```

# Actor output Schema

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

No description

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

No description

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

No description

## `changes` (type: `string`):

No description

## `summary` (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 = {
    "locations": [
        "Singapore"
    ],
    "urls": [
        "https://www.traveloka.com/en-sg/hotel/search?spec=01-12-2026.02-12-2026.1.1.HOTEL_GEO.107493.Singapore.2",
        "https://www.traveloka.com/en-sg/hotel/singapore/v-hotel-lavender-254056"
    ],
    "fetchReviews": false,
    "maxReviews": 10,
    "aggregateRatings": true,
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/traveloka-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 = {
    "locations": ["Singapore"],
    "urls": [
        "https://www.traveloka.com/en-sg/hotel/search?spec=01-12-2026.02-12-2026.1.1.HOTEL_GEO.107493.Singapore.2",
        "https://www.traveloka.com/en-sg/hotel/singapore/v-hotel-lavender-254056",
    ],
    "fetchReviews": False,
    "maxReviews": 10,
    "aggregateRatings": True,
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/traveloka-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 '{
  "locations": [
    "Singapore"
  ],
  "urls": [
    "https://www.traveloka.com/en-sg/hotel/search?spec=01-12-2026.02-12-2026.1.1.HOTEL_GEO.107493.Singapore.2",
    "https://www.traveloka.com/en-sg/hotel/singapore/v-hotel-lavender-254056"
  ],
  "fetchReviews": false,
  "maxReviews": 10,
  "aggregateRatings": true,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call abotapi/traveloka-com-scraper --silent --output-dataset

```

## MCP server setup

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