# Traveloka Hotel & Accommodation Scraper (`fanndev/traveloka-hotel-scraper`) Actor

Scrape Traveloka hotels, villas and apartments: profile, coordinates, star rating, facilities, room inventory and a full month of nightly rates per property, with an automatic weekday-versus-weekend price comparison. HTTP-only, no browser.

- **URL**: https://apify.com/fanndev/traveloka-hotel-scraper.md
- **Developed by:** [Faisal Ahdan naufal](https://apify.com/fanndev) (community)
- **Security:** Limited permissions
- **Stats:** 7 total users, 5 monthly users, 87.2% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.20 / 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?

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 Hotel & Accommodation Scraper

Pulls hotels, villas and apartments from Traveloka as structured JSON: the full
property profile, room inventory, and **a whole month of nightly rates per
property** with an automatic weekday-versus-weekend comparison.

It talks to Traveloka's own JSON API rather than rendering pages in a browser, so
a property costs a handful of HTTP requests instead of a headless Chrome session.

### What you get per property

| Group | Fields |
| --- | --- |
| Identity | `hotelId`, `name`, `displayName`, `url`, `accommodationType` (Hotel / Villa / Apartment), `starRating` |
| Location | `address`, `city`, `province`, `country`, `postalCode`, `latitude`, `longitude`, `areaName`, `areaGeoId` |
| Property | `checkInTime`, `checkOutTime`, `facilityCodes` + `facilityNames` (100+ per property), `supportedRateTypes`, `images` |
| Rooms | `rooms[]` — name, description, bedding, room size, occupancy, view, smoking policy, amenities, photos |
| Pricing | `priceCalendar[]` — one entry per night: `date`, `dayOfWeek`, `isWeekendNight`, `price`, `availabilityStatus`, `isCheapestOfMonth` |
| Pricing summary | `priceSummary` — `minPrice`/`maxPrice` with their dates, `averagePrice`, `weekdayAveragePrice`, `weekendAveragePrice`, `weekendPremiumPercent`, `nightsUnavailable` |
| Reviews | `reviewSummary` — `rating`, `ratingScale`, `reviewCount`, `ratingLabel` |

`weekendPremiumPercent` is the number most revenue managers actually want: how
much more a competitor charges for a Friday or Saturday night than for a weekday.

### Choosing properties

Four ways in, and you can mix them:

- **Property URLs** — paste Traveloka hotel pages. Only the numeric ID at the end
  of the URL matters, so shortened or re-slugged links still work.
- **Property IDs** — bare numeric IDs from an earlier run.
- **Property name searches** — matches **property names**, not cities. `Kempinski
  Jakarta` returns that hotel; `hotels in Jakarta` returns nothing useful. This is
  the right tool for assembling a named competitor set.
- **Sitemap sweep** — samples property IDs from Traveloka's public sitemaps for
  broad market analysis. Use `sitemapSkipLeaves` so repeat runs sample different
  properties instead of the same first page.

### What this actor cannot do, and why

**There is no "every hotel in city X, with prices" mode.** Traveloka's search-list
endpoint, and the dynamic per-room rate endpoint behind it, are protected by AWS
WAF with the `captcha` action — a visual challenge that no headless client can
answer. This was verified from clean residential Indonesian IPs as well as from
datacenter IPs, on the first request of a fresh session with a valid WAF
challenge token: the answer is `captcha` every time. It is a structural rule, not
an IP-reputation problem, so proxies do not change it.

What that rules out, concretely:

- listing all properties in a city or around a map area;
- per-room nightly rates and per-room discount/strikethrough prices.

What replaces it: the **price calendar**, which is not WAF-guarded and returns the
property's nightly lead-in rate for every date in a month. For tracking a
competitor set's pricing over time this is strictly more useful than a single
search snapshot — one request gives you 31 data points instead of one.

`starRating` is the official star classification. `reviewSummary.rating` is the
guest score out of 10. `availabilityStatus` of `NO_ALLOTMENT` means sold out for
that date, which is itself a useful demand signal.

### Cost and tuning

Requests per property: 1 profile + 1 review summary + 1 room list + 1 per calendar
month. A 12-month pull is 15 requests. Turn off `includeRooms` or
`includeReviewSummary` for leaner runs, and keep `calendarMonths` at 1 unless you
need the forward curve.

Traveloka's JSON API answers datacenter IPs normally, so the default Apify proxy
is fine. Residential Indonesian proxies do **not** unlock anything extra here.

### Output record types

Every record carries a `recordType` so failures are visible rather than silent:

- `PROPERTY` — scraped successfully.
- `NOT_FOUND` — the ID does not exist. Traveloka does not 404 for these; it echoes
  the ID back with every other field null, which this actor detects.
- `GATED` — an endpoint answered with an AWS WAF challenge.
- `ERROR` — a request failed; see `error`.

Partial failures do not lose the record: anything that could not be fetched is
listed in `warnings` and the rest of the property is still returned.

### A note on robots.txt

Traveloka's `robots.txt` disallows `/api/`, which is where this actor reads from.
It allows `/*/sitemap/**`, which is what the sitemap sweep uses. Decide for
yourself whether that fits your use of the data, your jurisdiction and your
agreement with Traveloka. Keep request rates modest and scrape only what you need.

# Actor input Schema

## `hotelUrls` (type: `array`):

Traveloka hotel, villa or apartment page URLs, for example https://www.traveloka.com/id-id/hotel/indonesia/hotel-borobudur-jakarta-7584. The numeric ID at the end of the URL is what matters; the slug is ignored.

## `hotelIds` (type: `array`):

Bare numeric Traveloka property IDs, if you already have them from a previous run.

## `searchQueries` (type: `array`):

Property names to look up, one per line, for building a competitor set. This matches PROPERTY NAMES, not cities: 'Kempinski Jakarta' works, 'hotels in Jakarta' does not. Listing every hotel in a city is not available (see README).

## `maxPropertiesPerQuery` (type: `integer`):

How many matching properties to keep from each entry in 'Property name searches'.

## `sitemapSweepLimit` (type: `integer`):

Collect this many property IDs from Traveloka's public hotel sitemaps for broad market sampling. Leave at 0 to skip. Each sitemap leaf holds 5,000 properties.

## `sitemapSkipLeaves` (type: `integer`):

Start the sitemap sweep this many leaf files in, so repeat runs sample different properties instead of the same first page.

## `maxProperties` (type: `integer`):

Hard cap on how many properties this run scrapes, across all input sources.

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

Stay date in YYYY-MM-DD, used for the room list and as the first month of the price calendar. Defaults to 30 days from today.

## `nights` (type: `integer`):

Length of stay used when requesting the room list.

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

Number of adult guests priced for. Rates change with occupancy.

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

Number of rooms priced for.

## `calendarMonths` (type: `integer`):

How many consecutive months of nightly rates to pull per property, starting at the check-in month. One request per month; each returns up to 31 nightly prices.

## `includePriceCalendar` (type: `boolean`):

Pull nightly rates and the weekday-versus-weekend summary. Turn off for a faster profile-only run.

## `includeRooms` (type: `boolean`):

Pull room names, photos, bedding, size and amenities. Note that per-room nightly rates are not available (see README); rates come from the price calendar.

## `includeReviewSummary` (type: `boolean`):

Pull the overall guest rating and review count for each property.

## `locale` (type: `string`):

Traveloka locale. This drives the language of facility names, room descriptions and labels.

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

Currency for all prices in the output.

## `requestDelaySeconds` (type: `string`):

Seconds to pause between API calls. Raise it if you see repeated 429 or 5xx responses.

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

Proxy settings. Residential is the default: Traveloka's AWS WAF answers a captcha to the session bootstrap on most Apify datacenter IPs. If you pick a datacenter group, the actor tries it first and falls back to residential sessions when it is challenged.

## Actor input object example

```json
{
  "hotelUrls": [
    {
      "url": "https://www.traveloka.com/id-id/hotel/indonesia/hotel-indonesia-kempinski-jakarta-2000000113168"
    }
  ],
  "searchQueries": [
    "Hotel Borobudur Jakarta"
  ],
  "maxPropertiesPerQuery": 10,
  "sitemapSweepLimit": 0,
  "sitemapSkipLeaves": 0,
  "maxProperties": 50,
  "nights": 1,
  "adults": 2,
  "rooms": 1,
  "calendarMonths": 1,
  "includePriceCalendar": true,
  "includeRooms": true,
  "includeReviewSummary": true,
  "locale": "en-id",
  "currency": "IDR",
  "requestDelaySeconds": "0.4",
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `datasetOverview` (type: `string`):

No description

## `pricingView` (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 = {
    "hotelUrls": [
        {
            "url": "https://www.traveloka.com/id-id/hotel/indonesia/hotel-indonesia-kempinski-jakarta-2000000113168"
        }
    ],
    "searchQueries": [
        "Hotel Borobudur Jakarta"
    ],
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("fanndev/traveloka-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 = {
    "hotelUrls": [{ "url": "https://www.traveloka.com/id-id/hotel/indonesia/hotel-indonesia-kempinski-jakarta-2000000113168" }],
    "searchQueries": ["Hotel Borobudur Jakarta"],
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("fanndev/traveloka-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 '{
  "hotelUrls": [
    {
      "url": "https://www.traveloka.com/id-id/hotel/indonesia/hotel-indonesia-kempinski-jakarta-2000000113168"
    }
  ],
  "searchQueries": [
    "Hotel Borobudur Jakarta"
  ],
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call fanndev/traveloka-hotel-scraper --silent --output-dataset

```

## MCP server setup

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