# PropertyGuru / DDProperty Scraper (SG · MY · TH) (`scrapyx/propertyguru-properties-scraper`) Actor

Scrapes property listings from PropertyGuru Singapore, PropertyGuru Malaysia and DDProperty Thailand — one actor, three SEA markets sharing one platform, 630,000+ listings. Filter by keyword, type, price, bedrooms and size; returns price, beds, baths, size, address, agent, photos and transit info.

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

## Pricing

from $0.0007 / actor start

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

## PropertyGuru / DDProperty Scraper (SG · MY · TH)

Scrapes property listings from **PropertyGuru Singapore**, **PropertyGuru
Malaysia** and **DDProperty Thailand** — Southeast Asia's largest property
portal group, with **over 630,000 listings** across the three markets.

One actor, three markets. They run on the same platform, so results share an
identical field shape and can be compared directly.

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

### What you get

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

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

Search rows are already rich (48 fields): price with currency, bedrooms,
bathrooms, floor area, full address, property type, agent and agency, photo
carousel, listing features, posting date, and transit info (MRT/BTS proximity
where the market has it).

Turn on **Fetch full listing details** for the complete description, full
facility list, project data and agent contact — one extra request per listing.

#### `SEARCH_SUMMARY` — one row per market

Upstream's own match total, how deep the run paged, the filters you requested,
and **the platform's own echo of the filters it actually parsed**.

### Input

| Field | What it does |
| --- | --- |
| **Markets** | Thailand, Singapore, Malaysia — pick any combination |
| **For sale / for rent** | which side of the market |
| **Keyword / location** | district or project name: `sukhumvit`, `orchard`, `mont kiara` |
| **Property type** | Condo, Apartment, Bungalow, Semi-detached, Land |
| **Min / max price** | in each market's own currency (THB / SGD / MYR) |
| **Bedrooms** | minimum count |
| **Minimum floor area** | sqm in TH/MY, sqft in SG |
| **Max listings per market** | `0` = unlimited |
| **Fetch full listing details** | off by default — the search row is already detailed |

#### Example

```json
{
  "markets": ["th", "sg", "my"],
  "listingType": "sale",
  "propertyType": "CONDO",
  "minPrice": 1000000,
  "bedrooms": 2,
  "maxItems": 500
}
```

### The thing this actor protects you from

**Property-type codes are market-specific, and an unsupported one is silently
ignored.** Ask for `LAND` in Thailand and you get 10,729 plots. Ask for `LAND`
in Singapore and you get **all 57,647 listings** — no error, no warning, just a
search that quietly wasn't filtered.

This actor checks the platform's own echo of what it parsed and tells you:

```json
"filtersRequested":     {"property_type_code": "LAND"},
"propertyTypeApplied":  false,
"upstreamSearchParams": {"locale": "en", "listingType": "sale", "page": 1}
```

plus a warning in the log. When the filter *does* land, the echo proves it:

```json
"upstreamSearchParams": {"propertyTypeGroup": "N", "propertyTypeCode": ["CONDO"],
                         "minPrice": 1000000, "bedrooms": [2]}
```

| code | Thailand | Singapore | Malaysia |
| --- | --- | --- | --- |
| Condo / Apartment | ✅ | ✅ | ✅ |
| Bungalow / Semi-detached | ✅ | ✅ | ✅ |
| Land | ✅ | ✗ | ✗ |
| Terraced / HDB | ✗ | ✗ | ✗ |

Unsupported markets and impossible price ranges are refused before the first
request rather than producing a healthy-looking wrong result.

### Notes on reliability

- **No bot challenge on search.** All three hosts sit behind Cloudflare but
  serve data cold.
- **Singapore requires a Referer header** on paginated URLs (it answers 403
  without one), as do Thai detail pages. The actor always sends the right one —
  you never see this.
- **Runs unattended.** No session, no token, nothing to refresh.
- **Failures never vanish.** A listing withdrawn mid-crawl degrades that one
  row (`_detailError`); every input maps to at least one output row.
- **Residential proxy by default** on cloud runs.

### Output envelope

Every record carries `_input`, `_source` and `_scrapedAt`. Upstream field names
pass through **verbatim** — no renaming. `_source` is `S1-nextdata-search`, or
`S1-nextdata-search+S2-nextdata-detail` once details are attached.

The market you searched is `marketQueried` (upstream's own fields are left
untouched).

See [`CRAWLING_METHOD.md`](CRAWLING_METHOD.md) for the full reverse-engineering
trail — the shared-buildId proof, the Referer gate, and the per-market
property-type matrix.

# Actor input Schema

## `markets` (type: `array`):

Which markets to search — one search per market, each with its own SEARCH\_SUMMARY row. All three run on the same platform, so results share an identical field shape and can be compared directly.

## `listingType` (type: `string`):

Which side of the market to search.

## `keyword` (type: `string`):

Free-text search, e.g. a district or project name: bangkok, sukhumvit, orchard, mont kiara. Leave empty to browse the whole market.

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

IMPORTANT: these codes are market-specific. Condo and Apartment work on all three markets; Bungalow and Semi-detached work on all three; Land only narrows Thailand; Terraced and HDB are not honoured on any market as of 2026-08-13. An unsupported code is silently dropped upstream and returns an UNFILTERED search — this actor detects that and reports propertyTypeApplied=false on the summary row rather than letting it pass as filtered.

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

In the market's own currency (THB / SGD / MYR). Leave empty or 0 for no minimum.

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

In the market's own currency (THB / SGD / MYR). Leave empty or 0 for no maximum.

## `bedrooms` (type: `integer`):

Minimum number of bedrooms. Leave empty or 0 for any.

## `minSize` (type: `integer`):

In the market's own unit (sqm in TH/MY, sqft in SG). Leave empty or 0 for any.

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

Stop paginating a market after this many listings. Set to 0 for unlimited — note Malaysia alone advertises over 224,000 listings for sale, so set a bound unless you mean it.

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

Fetch each listing's own page for the full description, complete facility list, project data and agent contact — one extra request per listing. Off by default because the search row is already rich (price, beds, baths, size, address, agent, photos, features, MRT/transit info), and detail pages are the slowest part of any run.

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

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

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

All three hosts sit behind Cloudflare. No challenge was served on a direct connection, but Residential is the default for cloud runs — container egress is a different posture, and this portfolio has lost a full cloud run to that difference before.

## Actor input object example

```json
{
  "markets": [
    "th"
  ],
  "listingType": "sale",
  "keyword": "",
  "propertyType": "",
  "minPrice": 0,
  "maxPrice": 0,
  "bedrooms": 0,
  "minSize": 0,
  "maxItems": 100,
  "includePropertyDetails": false,
  "maxConcurrency": 4,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# 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 = {
    "markets": [
        "th"
    ]
};

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

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

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scrapyx/propertyguru-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/397eVZwueTswOfWMM/builds/ugGrm1niWrBHwwsMd/openapi.json
