# Rumah123 Property Scraper (Indonesia) (`scrapyx/rumah123-properties-scraper`) Actor

Scrapes houses and apartments for sale or rent from Rumah123, Indonesia's #1 property portal. Search any city or district; returns price, address, bedrooms, bathrooms, land/building size, certificate type, full description, photos and agent details.

- **URL**: https://apify.com/scrapyx/rumah123-properties-scraper.md
- **Developed by:** [Ibnu Adzim](https://apify.com/scrapyx) (community)
- **Categories:** Real estate, Lead generation, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.56 / 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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

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

## How to integrate an Actor?

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

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

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

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

# README

## Rumah123 Property Scraper (Indonesia)

Scrapes houses, apartments, land and shop-houses for sale or rent from
**[Rumah123](https://www.rumah123.com)** — Indonesia's #1 property portal,
part of the PropertyGuru group but running its own older codebase (a
different stack than `propertyguru-properties-scraper` elsewhere in this
portfolio, which covers Singapore/Malaysia/Thailand).

Public data only. No login, no cookies, no browser.

### What you get

Three record types share one dataset, told apart by `recordType`.

#### `PROPERTY` — one row per listing

Search rows (`listing`) carry name, address, bedrooms, bathrooms, floor size,
coordinates and a thumbnail — Schema.org `ItemList` data, straight from the
search page. With **Fetch full property details** on (the default), each row
also gets a `propertyDetails` object:

- `ldJson` — price (`offers.price`, IDR), seller/agent name and profile URL,
  the full photo gallery, description, geo
- `specs` — land size, building size, condition/renovation year, bedrooms,
  bathrooms, garage/carport, floor count, certificate type (SHM/HGB/etc.) —
  parsed from a server-rendered HTML table the JSON-LD does not carry

#### `SEARCH_SUMMARY` — one row per location searched

How many pages were fetched, how many properties matched, and — if you set a
price or bedroom filter — how many matched *before* that client-side filter
was applied.

### Input

| Field | What it does |
| --- | --- |
| **Jual / sewa** | for sale or for rent |
| **Property type** | rumah (house) or apartemen (apartment) — see note 3 below on why tanah/ruko are not offered |
| **Locations** | one search per entry: `jakarta-selatan`, `bandung`, `surabaya`… human spellings are slugified for you |
| **Property URLs** | scrape specific listings directly, skipping search (full URL required — see below) |
| **Min / max price (IDR)** | client-side filter, forces detail-fetch on |
| **Min bedrooms** | client-side filter, works with or without detail-fetch |
| **Max properties / max pages per location** | pagination caps |
| **Fetch full property details** | off = fast listing-only crawl, 1 request per ~9 properties |

#### Example

```json
{
  "transactionType": "jual",
  "propertyType": "rumah",
  "locations": ["jakarta-selatan", "bandung"],
  "minPrice": 500000000,
  "minBedrooms": 3,
  "maxItems": 200,
  "includePropertyDetails": true
}
```

### Things this actor is honest about

**1. Price and bedroom filters are applied client-side, not sent upstream.**
Rumah123's `robots.txt` explicitly disallows crawling with `minPrice=`,
`bedroom=`, `sort=` and similar query parameters for generic user agents (it
grants a *more* permissive rule set to a named group of AI crawlers —
ClaudeBot, GPTBot, etc. — but this actor runs as a generic browser-
impersonating client like every other actor in this portfolio, so it honours
the stricter rule). Setting `minPrice`/`maxPrice`/`minBedrooms` filters the
*parsed dataset* after fetching instead of changing the request — same
result for you, zero disallowed requests. Every `SEARCH_SUMMARY` row reports
`matchedBeforeClientSideFilter` so you can see how much was filtered out.

**2. Price only exists on the detail page.** The search page's JSON-LD never
carries price, so `minPrice`/`maxPrice` automatically force **Fetch full
property details** on even if you left it off — otherwise the filter would
silently keep zero rows.

**3. `tanah` (land) and `ruko` (shop-house) are not offered.** Both return
HTTP 200, but their search page's only JSON-LD is a single generic
`RealEstateListing` page description — no per-listing array to iterate.
Individual parcels would need HTML-card scraping, a genuinely different
extraction path not built this session, so these two are deliberately left
out of the input schema rather than shipped broken. `rumah`/`apartemen` both
carry a proper `ItemList` with one entry per listing.

### Notes on reliability

- **No WAF challenge.** Serves data cold on every TLS profile tested (5
  profiles × home page, plus every search/detail/bogus-path combination
  tried with `chrome131`).
- **Runs unattended.** No human-captured session, no expiring token.
- **Unknown location or property type fails cleanly** as `not_found` — a
  clean HTTP 404, never a silently-wrong nationwide baseline (unlike
  Immoweb/Otodom/SUUMO elsewhere in this portfolio, all of which have a
  silent-widen trap on bad filters).
- **No upstream pagination ceiling found.** `page=999` on a narrow search
  still returned fresh, different listings rather than clamping to the last
  real page (the Bayt trap) or going empty (the OnTheMarket ceiling) — so
  `maxPages` is this actor's own safety valve, not a discovered limit.
- **Residential proxy by default** on cloud runs.
- **Failures never vanish.** A listing withdrawn mid-crawl degrades that one
  row (`_detailError`); every input maps to at least one output row.

### Output envelope

Every record carries `_input`, `_source` and `_scrapedAt`. Upstream field
names pass through **verbatim** under `listing` (search) and
`propertyDetails.ldJson` (detail) — no renaming. `_source` is
`S1-jsonld-search` for listing-only rows and `S1-jsonld-search+S2-jsonld-detail`
once details are attached.

Direct-mode `propertyUrls` requires the **full URL** — Rumah123's path embeds
an SEO slug (`.../jakarta-selatan-tebet/rumah-baru-.../hos42193485/`) that
cannot be reconstructed from the numeric id alone, unlike some other actors
in this portfolio.

See [`CRAWLING_METHOD.md`](CRAWLING_METHOD.md) for the full reverse-engineering
trail, including the robots.txt AI-crawler-group finding and the specs-table
extraction approach.

# Actor input Schema

## `transactionType` (type: `string`):

Which side of the market to search.

## `propertyType` (type: `string`):

'rumah' and 'apartemen' are the only types verified this session: both carry a proper per-listing ItemList on the search page. 'tanah' (land) and 'ruko' (shop-house) return HTTP 200 but their search page's only JSON-LD is a generic page description with no per-listing array -- individual parcels would need HTML-card scraping, not built yet -- so they are intentionally not offered here rather than shipped broken.

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

Rumah123 location slugs -- one search per entry, each with its own SEARCH\_SUMMARY row. Cities and districts both work: jakarta-selatan, bandung, surabaya, tangerang, jakarta-selatan-tebet. A human spelling like 'Jakarta Selatan' is slugified automatically. An unknown location returns a clean 404 reported as not\_found, never silently-wrong results (verified -- unlike several other real-estate targets in this portfolio, Rumah123 does not fall back to an unfiltered baseline on a bad path segment).

## `propertyUrls` (type: `array`):

Scrape specific listings directly, skipping search. Requires the FULL property URL (e.g. https://www.rumah123.com/properti/jakarta-selatan-tebet/rumah-baru-kalcer-skena-di-jatipadang-pasming-hos42193485/) -- a bare numeric id cannot be used because Rumah123's URL embeds an SEO slug that is not derivable from the id alone. Can be combined with a location search in the same run.

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

Leave empty or 0 for no minimum. Applied CLIENT-SIDE after fetching (never sent to Rumah123 as a query parameter -- their robots.txt disallows price filter params for generic crawlers). Setting this automatically turns on 'Fetch full property details', since price only exists on the detail page.

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

Leave empty or 0 for no maximum. Same client-side behaviour as Minimum price.

## `minBedrooms` (type: `integer`):

Leave empty or 0 for any. Applied client-side; this field is already present on the search-page listing, so it works whether or not 'Fetch full property details' is on.

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

Stop paginating a location after this many properties. Set to 0 for unlimited (still bounded by 'Max pages per location').

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

Hard cap on pagination depth per location, independent of maxItems. No upstream page ceiling was found during recon (page 999 still returned fresh, different listings on a narrow search), so this is the actor's own safety valve against runaway crawls on very broad searches.

## `includePropertyDetails` (type: `boolean`):

Fetch price, seller/agent info, the full photo gallery, description and specs (land size, building size, garage, floors, certificate type) for each result via one extra request per property. Turn this off for a fast listing-only crawl -- search rows already carry address, bedrooms, bathrooms, floor size and a thumbnail. Forced on automatically if minPrice/maxPrice is set, since price is only available on the detail page.

## `maxConcurrency` (type: `integer`):

Upper bound on requests in flight at once, across location pagination and detail fetches.

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

Rumah123 showed no bot challenge during local recon, but the first real cloud run hit a Cloudflare interstitial 4/4 times on the default (non-geo-targeted) Residential pool -- both with and without proxy, so it wasn't a bad individual IP. Pinning the proxy to Indonesia fixed it immediately (verified: 5/5 clean). Country is pinned by default for that reason -- changing it away from Indonesia is likely to reintroduce the challenge.

## Actor input object example

```json
{
  "transactionType": "jual",
  "propertyType": "rumah",
  "locations": [
    "jakarta-selatan"
  ],
  "propertyUrls": [],
  "minPrice": 0,
  "maxPrice": 0,
  "minBedrooms": 0,
  "maxItems": 90,
  "maxPages": 30,
  "includePropertyDetails": true,
  "maxConcurrency": 4,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "ID"
  }
}
```

# Actor output Schema

## `items` (type: `string`):

One row per scraped record. See the dataset's default view for field definitions.

# 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": [
        "jakarta-selatan"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapyx/rumah123-properties-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": ["jakarta-selatan"] }

# Run the Actor and wait for it to finish
run = client.actor("scrapyx/rumah123-properties-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": [
    "jakarta-selatan"
  ]
}' |
apify call scrapyx/rumah123-properties-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scrapyx/rumah123-properties-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/0cpY1AMXo5O51Rata/builds/wegAFrpML0dGlKwJh/openapi.json
