# DDProperty TH $1💰 Powerful Filters + Deep Search (`abotapi/ddproperty-scraper`) Actor

From $1/1K. Extract property listings from ddproperty.com Thailand at scale. Get comprehensive data including prices, features, images, agent contacts, coordinates, nearby transit (BTS/MRT), and more. Perfect for Thai real estate analytics, market research, and investment analysis.

- **URL**: https://apify.com/abotapi/ddproperty-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Categories:** Real estate, Developer tools, Automation
- **Stats:** 32 total users, 4 monthly users, 93.2% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

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

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

## DDProperty Thailand Scraper

Extract property listings from **ddproperty.com** (PropertyGuru Thailand) at scale. Get comprehensive data including prices, features, images, agent contacts, coordinates, nearby transit (BTS/MRT), and more. Perfect for Thai property market analytics, research, and investment analysis.

### What does DDProperty Scraper do?

This Apify Actor scrapes property listings from ddproperty.com. It extracts full listing data from search result pages and automatically enriches every listing with exact coordinates via the PropertyGuru Map Cluster API, so no individual page visits are required.

#### Key Features

| Feature | Description |
|---------|-------------|
| **All Listing Types** | For Sale and For Rent properties across Thailand |
| **Property Type Filters** | Condos, Houses, Villas, Townhouses, Land, Apartments, Commercial |
| **Price & Bedroom Filters** | Narrow results by price range (THB) and bedrooms |
| **Location Filters** | Filter by region (Bangkok, Phuket, Chiang Mai, Pattaya, etc.) |
| **Exact Coordinates** | Lat/lng for every listing via PropertyGuru Map Cluster API (no detail page visits) |
| **MRT/BTS Fallback** | Remaining listings enriched with nearest transit station coordinates (253 stations) |
| **Multiple Dataset Views** | Overview, Map View, Detailed, Agent Contacts |
| **Pagination** | Multi-page scraping with automatic retry on blocked pages |

### What data can you extract?

The scraper extracts 30+ fields per listing:

<table>
<tr><td>

**Property Details**

- Listing ID & URL
- Title & full address
- Property type (Condo, House, Villa, etc.)
- Bedrooms & bathrooms
- Floor area & land area
- Tenure (Freehold/Leasehold)
- Badges (New Project, Built year, etc.)

</td><td>

**Pricing**

- Price (THB numeric value)
- Formatted price (e.g. "฿12,990,000")
- Price per sqm
- Listing type (Sale/Rent)

</td></tr>
<tr><td>

**Location & Transit**

- Latitude & longitude (exact, from Map Cluster API)
- Region, district, area (with codes)
- Nearest MRT/BTS station name & line
- Distance to nearest station
- Location source indicator

</td><td>

**Agent & Developer**

- Agent name & ID
- Agency ID
- Developer name
- Is developer listing flag

</td></tr>
<tr><td>

**Media**

- Property images (up to 10 per listing)
- Image count

</td><td>

**Dates & Metadata**

- Posted date (text & unix timestamp)
- Recency text
- Listing status (Active, etc.)

</td></tr>
</table>

### Quick Start

Search for condos for sale in Bangkok:

```json
{
  "mode": "search",
  "listing_type": "sale",
  "property_type": "CONDO",
  "region": "TH10",
  "max_properties": 20,
  "max_pages": 3,
  "proxy": { "useApifyProxy": true }
}
```

### How to Use

#### Step 1: Basic search by region

```json
{
  "mode": "search",
  "listing_type": "sale",
  "region": "TH83",
  "max_properties": 50,
  "max_pages": 5,
  "proxy": { "useApifyProxy": true }
}
```

#### Step 2: Filter by property type and price

```json
{
  "mode": "search",
  "listing_type": "rent",
  "property_type": "CONDO",
  "region": "TH10",
  "min_price": 15000,
  "max_price": 50000,
  "bedrooms": 1,
  "max_properties": 100,
  "max_pages": 10,
  "proxy": { "useApifyProxy": true }
}
```

#### Step 3: Scrape specific URLs

```json
{
  "mode": "url",
  "urls": [
    "https://www.ddproperty.com/en/condo-for-sale/in-bangkok-th10",
    "https://www.ddproperty.com/en/property-for-rent/in-phuket-th83"
  ],
  "max_properties": 50,
  "max_pages": 5,
  "proxy": { "useApifyProxy": true }
}
```

### Input Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `mode` | string | `search` | `search` (build URL from filters) or `url` (use provided URLs) |
| `urls` | array | `[]` | DDProperty search URLs (url mode only) |
| `listing_type` | string | `sale` | `sale` or `rent` |
| `property_type` | string | - | `CONDO`, `BUNG`, `SEMI`, `VIL`, `TOWN`, `LAND`, `APT`, `RET`, `OFF`, `WAR`, `BIZ`, `SHOP` |
| `region` | string | - | Region code (see table below) |
| `min_price` | integer | - | Minimum price in THB |
| `max_price` | integer | - | Maximum price in THB |
| `bedrooms` | integer | - | Number of bedrooms (0 = Studio) |
| `sort` | string | `date` | Sort by: `date`, `price`, `psf` |
| `sort_order` | string | `desc` | `asc` or `desc` |
| `max_properties` | integer | `20` | Max listings to scrape (0 = unlimited) |
| `resumeFromRunId` | string | - | Continue one interrupted run/dataset (skips already-collected properties) |
| `incrementalMode` | boolean | `false` | Recurring monitoring: remembers the previous run of this search and returns only what changed |
| `stateKey` | string | - | Optional manual key to control which runs share incremental state |
| `emitUnchanged` | boolean | `false` | Incremental mode: also return properties with no detected change |
| `emitExpired` | boolean | `false` | Incremental mode: also return properties no longer found (after a complete scan) |
| `proxy` | object | - | Proxy configuration (Apify proxy recommended) |

### Region Codes

| Code | Region | Code | Region |
|------|--------|------|--------|
| `TH10` | Bangkok | `TH83` | Phuket |
| `TH11` | Samut Prakan | `TH84` | Surat Thani (Koh Samui) |
| `TH12` | Nonthaburi | `TH77` | Prachuap Khiri Khan (Hua Hin) |
| `TH13` | Pathum Thani | `TH50` | Chiang Mai |
| `TH20` | Chon Buri (Pattaya) | `TH90` | Songkhla |

### Property Type Codes

| Code | Type | Code | Type |
|------|------|------|------|
| `CONDO` | Condo | `LAND` | Land |
| `BUNG` | Single Detached House | `APT` | Apartment |
| `SEMI` | Semi-Detached House | `RET` | Retail Space |
| `VIL` | Villa | `OFF` | Office Space |
| `TOWN` | Townhouse | `SHOP` | Shophouse |

### Resume an interrupted run

Paste a previous run ID or dataset ID into `resumeFromRunId` to continue that ONE crawl — properties it already collected are skipped instead of re-scraped. This is a one-off continuation, unrelated to Incremental mode below.

### Incremental mode (recurring monitoring)

Turn on `incrementalMode` to schedule this actor against the same search repeatedly and get only what changed. The actor remembers the previous run of this search itself (a saved state, keyed on your filters or on `stateKey`) and tags every property returned with a `changeType`:

| `changeType` | Meaning |
|---|---|
| `NEW` | First time this property has ever been tracked under this state key |
| `UPDATED` | Tracked before; at least one comparable field changed (see `changedFields`) |
| `UNCHANGED` | Tracked before, nothing changed — suppressed (not returned) unless `emitUnchanged` is ON |
| `REAPPEARED` | Was previously marked EXPIRED, now back |
| `EXPIRED` | Was tracked and present before, no longer found — only emitted when `emitExpired` is ON, and only after a run that scanned its search to a genuine natural end (no `max_properties` cap hit, no Resume) |

Additional fields in incremental mode: `changedFields` (which field names differ from the last tracked scrape), `firstSeenAt`, `lastSeenAt` (UTC timestamps).

`stateKey` is optional — leave it blank to auto-derive the tracked scope from your search filters (mode, listing/property type, search text, region, price/bedroom filters, sort, URLs, and whether detail pages are enabled). Two different filter setups never share a baseline unless you set the same `stateKey` on both.

`emitUnchanged` and `emitExpired` default OFF. Turning either on returns — and bills, at the standard per-result rate — extra rows beyond what actually changed.

**Volatile fields excluded from change detection:** `recency` (e.g. "Listed on Jul 07, 2026 (3w ago)") carries a relative "time ago" phrase that advances on its own as time passes, even when nothing about the listing changed — it is excluded from the NEW/UPDATED/UNCHANGED comparison (but still returned in every record) so it never causes a false UPDATED. `images`, `images_full`, `facilities`, `amenities`, `floor_plans`, and `highlights` are compared by membership, not order — a pure reshuffle of these lists is not treated as a change (their emitted order is untouched).

### 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 item 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.

### Output Schema

Each scraped listing is returned as a JSON object with the fields shown below. Field names and types are stable across all results; values shown are placeholder examples.

```json
{
  "id": "<integer listing id>",
  "title": "<localized listing title>",
  "address": "<full property address>",
  "listing_type": "sale | rent",
  "property_type": "CONDO | BUNG | SEMI | VIL | TOWN | LAND | APT | RET | OFF | WAR | BIZ | SHOP",
  "price": "<integer THB>",
  "price_formatted": "<formatted THB string, e.g. ฿X,XXX,XXX>",
  "price_per_sqm": "<formatted THB / sqm>",
  "bedrooms": "<integer; 0 = studio>",
  "bathrooms": "<integer>",
  "floor_area": "<sqm, number>",
  "tenure": "Freehold | Leasehold",
  "developer": "<developer name, when applicable>",
  "is_developer_listing": "<boolean>",
  "agent_name": "<agent display name>",
  "agent_id": "<integer>",
  "agency_id": "<integer>",
  "region": "<region label>",
  "region_code": "<TH region code>",
  "district": "<district label>",
  "district_code": "<TH district code>",
  "area": "<area label>",
  "area_code": "<TH area code>",
  "latitude": "<float>",
  "longitude": "<float>",
  "location_source": "nearest_mrt_station | map_api",
  "nearby_mrt": "<descriptive distance to nearest MRT/BTS>",
  "nearby_station_id": "<station id, e.g. BL17>",
  "badges": ["<string>", "..."],
  "posted_date": "<DD MMM YYYY>",
  "posted_unix": "<unix timestamp>",
  "images": ["<image url>", "..."],
  "image_count": "<integer>",
  "url": "<full listing url on ddproperty.com>",
  "status": "<listing status code, e.g. ACT>"
}
```

# Actor input Schema

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

Search mode = build URLs from the filters below. URL mode = paste one or more ddproperty.com search URLs (recommended for refined searches; pagination still auto-advances forward from any ?page=N you provide).

## `listing_type` (type: `string`):

sale or rent

## `property_type` (type: `string`):

Filter by property type code

## `search` (type: `string`):

Free-text location search (e.g. 'Sukhumvit', 'Ari', 'Phuket'). Use this OR the region code below, not both. If both are set, this takes priority.

## `region` (type: `string`):

Location code: region (TH10=Bangkok, TH83=Phuket), district (TH1033=Watthana), or area (TH103302=Khlong Tan).

## `min_price` (type: `integer`):

Minimum price in Thai Baht

## `max_price` (type: `integer`):

Maximum price in Thai Baht

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

Number of bedrooms (0=Studio)

## `sort` (type: `string`):

Sort results

## `sort_order` (type: `string`):

Sort direction: ascending or descending

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

One or more ddproperty.com search URLs. Open the site in a browser, refine your search with any filters, then paste the resulting URLs here. Pagination auto-advances forward from each URL - if a URL contains ?page=N, scraping continues from N. In URL mode, the search-mode fields above are ignored.

## `max_properties` (type: `integer`):

Maximum number of properties to scrape (0=unlimited). Caps both Search and URL modes. Pagination stops automatically when this cap is hit or the site runs out of results.

## `enable_detail_pages` (type: `boolean`):

Fetch the per-listing detail page in addition to the search page. Adds latitude/longitude, description, facilities, project info, etc.

## `detail_concurrency` (type: `integer`):

Parallel detail-page fetches. All concurrent requests share one proxy IP, so values above 3 risk per-IP rate limiting.

## `session_ttl_seconds` (type: `integer`):

How long to reuse a single edge session before forcing a refresh.

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

Paste a previous run ID or dataset ID to continue that ONE interrupted crawl - properties it already collected are skipped, not re-scraped. Unrelated to Incremental mode below, which tracks a recurring search across scheduled runs.

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

When ON, this actor remembers the last run of this SAME search (via its own saved state, keyed on your filters or on State key below) and classifies every property as NEW / UPDATED / REAPPEARED / EXPIRED. Unrelated to Resume from run/dataset ID above, which continues one specific interrupted run. Default OFF - normal runs return every matching property every time.

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

Optional. Set this to a name of your choice to manually control which runs share incremental state (e.g. to deliberately merge two different searches into one monitoring campaign). Leave blank to auto-derive the key from your search filters - two different filter setups then never share a baseline.

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

When ON, properties with no detected change are still returned (tagged changeType=UNCHANGED) instead of being suppressed. Off by default so a recurring run returns only what actually changed. Turning this ON returns - and bills, at the standard per-result rate - every property scanned, not just the changed ones.

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

When ON, properties tracked by a previous incremental run that no longer appear are returned once, tagged changeType=EXPIRED. Only fires after a run that scans its search to a genuine natural end (no Max properties cap hit, no Resume) - a partial run leaves previous state untouched instead of guessing. Turning this ON returns - and bills, at the standard per-result rate - these extra rows.

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

Residential proxy with country=TH is the default and recommended. The session is bound to a single proxy IP and is reused across requests internally.

## `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",
  "listing_type": "sale",
  "sort": "date",
  "sort_order": "desc",
  "urls": [
    "https://www.ddproperty.com/en/property-for-sale/in-bangkok-th10"
  ],
  "max_properties": 20,
  "enable_detail_pages": true,
  "detail_concurrency": 1,
  "session_ttl_seconds": 300,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "TH"
  },
  "maxNotifyListings": 50
}
```

# Actor output Schema

## `listings` (type: `string`):

Individual property listing records with address, price, features, location, and transit data.

## `output` (type: `string`):

Complete output with all properties and run metadata (duration, total scraped, engine).

## `metadata` (type: `string`):

Scraping run metadata including timing, counts, MRT enrichment stats, and configuration.

## `mrt_stations` (type: `string`):

Complete Thai transit station data (12 lines, 253 stations) with coordinates.

# 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",
    "urls": [
        "https://www.ddproperty.com/en/property-for-sale/in-bangkok-th10"
    ],
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "TH"
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/ddproperty-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",
    "urls": ["https://www.ddproperty.com/en/property-for-sale/in-bangkok-th10"],
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "TH",
    },
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/ddproperty-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",
  "urls": [
    "https://www.ddproperty.com/en/property-for-sale/in-bangkok-th10"
  ],
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "TH"
  }
}' |
apify call abotapi/ddproperty-scraper --silent --output-dataset

```

## MCP server setup

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