# DDproperty Thailand Scraper - Properties, Prices & Agents (`abotapi/ddproperty-scraper`) Actor

Scrape DDproperty.com Thailand listings at scale. Extract prices, property features, images, agent contacts, GPS coordinates, nearby BTS/MRT transit and more. Built 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
- **Features:** Limited permissions
- **Stats:** 46 total users, 8 monthly users, 91.4% runs succeeded, 1 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?

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

## DDproperty Scraper: Thailand Property Listings, Prices & Transit Data

DDproperty is Thailand's largest real estate listing site, and this scraper turns it into structured data. Get for sale and for rent listings across every Thai region with price, bedrooms, bathrooms, floor area, tenure, agent and developer details, exact coordinates, and nearest BTS or MRT station. Build filtered searches by region, property type, price and bedrooms, or paste your own DDproperty search URLs, then export to JSON, CSV or Excel, or pull results straight into your app through the API.

### Why This Scraper?

- **Two ways to search.** Build a search from filters (region, property type, price, bedrooms) or paste one or more DDproperty search URLs when you need a more refined query than the filters allow.
- **Detail-page enrichment on demand.** Turn on detail pages to add exact latitude and longitude, a full description, facilities, floor plans and project attributes to every listing, not just what the search results page shows.
- **Agent and developer data.** Every listing carries the agent name and ID, the agency ID, and the developer name (or the full legal name on new-launch project rows) when the listing belongs to a project.
- **Nationwide region coverage.** Search by region, district or area code, from Bangkok down to a single sub-district, or leave it open for every listing on a listing type.
- **Built for schedules.** Incremental mode remembers a recurring search itself and returns only new, updated and reappeared listings on later runs (plus expired ones if you turn on Emit expired), with unchanged ones suppressed by default.
- **Resume without re-billing.** Point Resume at a previous run or dataset ID and the actor skips every listing it already collected there.
- **Nearest transit.** Distance and station ID for the closest BTS or MRT stop whenever the listing shows one, useful for Bangkok's transit-driven property market.

### Use Cases

- **Property investment research:** compare prices per square meter, tenure and developer across regions to spot value.
- **Rental market monitoring:** track rent listings in a district over time with incremental mode to catch new units as they list.
- **Real estate lead generation:** collect agent names, agent IDs and agency IDs from listings matching your target criteria.
- **Proptech and map products:** feed exact coordinates and nearest-transit data into a map or search product.
- **Market research and reporting:** build a dataset of listing counts, price bands and property types by region for analysis.

### Data You Get

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

| Field | Example |
| --- | --- |
| `id` | `12345678` |
| `title` | `"Condo for Sale in Watthana, Bangkok"` |
| `address` | `"Sukhumvit Soi 24, Khlong Tan, Watthana, Bangkok"` |
| `listing_type` | `"sale"` (or `"rent"`) |
| `property_type` | `"CONDO"` |
| `price` / `price_formatted` | `8500000` / `"฿8,500,000"` |
| `price_per_sqm` | `"฿141,667 / sqm"` |
| `bedrooms` / `bathrooms` / `floor_area` | `2` / `2` / `60` |
| `tenure` | `"Freehold"` (or `"Leasehold"`) |
| `developer` / `is_developer_listing` | `"Sample Developer Co."` / `true` |
| `agent_name` / `agent_id` / `agency_id` | `"Agent Name"` / `445566` / `778899` |
| `region` / `region_code` / `district` / `area` | `"Bangkok"` / `"TH10"` / `"Watthana"` / `"Khlong Tan"` |
| `latitude` / `longitude` | `13.7305` / `100.5697` (when detail pages are on) |
| `nearby_mrt` / `nearby_station_id` | `"7 min (560 m) from E5 Asok BTS"` / `"E5"` |
| `badges` | `["New Project: 2026", "Freehold"]` |
| `posted_date` / `posted_unix` / `images` / `image_count` | `"07 Jul 2026"` / `1783382400` (the last (re-)post time) / `["https://.../image1.jpg"]` / `1` |
| `url` / `status` | full DDproperty link to the listing / `"ACT"` |
| `description` / `facilities` | full listing description / `["Swimming Pool", "Fitness Centre"]` (when detail pages are on) |
| `type` / `sourceSite` / `currency` | `"listing"` / `"ddproperty.com"` / `"THB"` |
| `furnishing` / `floor_level` / `project_developer` | `"Fully Furnished"` / `"High Floor"` / `"Sample Developer Co., Ltd."` (listing detail pages; `developer` holds the lister name, exactly like search rows, and `project_developer` the developer of the project the listing belongs to) |
| `completion_year` / `total_units` / `project_name` | `2008` / `219` / `"Sample Residence"` (listing and project detail pages) |
| `updated_date` / `expiry_date` / `first_posted_at` | `"2026-10-14 18:27:00"` / `"2026-11-15 18:27:00"` / `"2026-01-28 18:28:02"` (listing detail pages; `first_posted_at` is when the listing was first posted, while `posted_date` and `posted_unix` hold the last (re-)post time, exactly like search rows) |
| `unit_types` / `brochure_url` / `developer_full_name` | `[{"name": "1 Bedroom", "min_price": "฿ 8,900,000", ...}]` / PDF link / `"Sample Developer Co., Ltd."` (project detail pages; `developer_full_name` is the full legal developer name, also on new-launch project rows. On project detail rows `developer` holds the short name shown on search cards, while new-launch rows carry only `developer_full_name`) |
| `source_page` | `"listing_detail"`, `"project_detail"`, `"agent_profile"` or `"agent_directory"` (single-page links only) |
| Agent rows (`type` = `agent`) | `agent_id`, `agent_name`, `agency_name`, `agency_id`, `job_title`, `is_agent_verified`, `years_on_site`, `active_sale_listings`, `active_rent_listings` (directory and search rows), `profile_sale_listings`, `profile_rent_listings` (profile rows), `website`, `facebook_url`, `instagram_url`, `about`, `services`, `operating_areas`, `agent_photo`, `agent_phone` (directory cards only), `directory_section` |
| `changeType` / `changedFields` | incremental mode only: `NEW`, `UPDATED`, `UNCHANGED`, `REAPPEARED` or `EXPIRED`, and the fields that changed |

The dataset has seven views: overview, map, agents, detailed, project pages, agent rows and changes (incremental rows). Detail-page fields are empty when **Fetch detail pages** is off or a detail page could not be read. Listing rows (`type` = `listing`) come from listing pages, listing detail pages and project detail pages; agent profile and agent directory pages produce `agent` rows, identified by `agent_id` and tracked separately in incremental mode (an agent id never collides with a listing id). In incremental mode the new fields do not cause a one-time UPDATED wave on listings stored before they existed, but a field that appears later on a listing stored by this version does flag UPDATED. Agent directory pages show a rotating set, so they never trigger EXPIRED rows.

**Fields that never trigger UPDATED (incremental mode):** `recency`, a relative `posted_date` such as "2 days ago", and the query string of image URLs are left out of change detection but still appear in your output. You can add more with `ignoreFieldsForChanges`.

### How to Use

1. Pick a **Search mode**: `search` (build a URL from the filters below) or `url` (paste DDproperty search URLs directly).
2. In search mode, set listing type, property type, region or free text location, price and bedroom filters. In URL mode, paste your DDproperty search links. Supported URL shapes: region sale and rent, national, paginated (`?page=N`, scraping continues forward from N), filtered and sorted, price-landing, district, near-station, property-type, commercial and new-launch search pages. The prefilled list holds one example link per supported shape, but all links share the one **Max properties** budget (default 20) and are walked in the order listed: with the defaults the first link (a Bangkok sale search, about 20 cards per page) fills the whole budget and the links after it are not reached. Raise **Max properties**, or remove links, to reach more shapes; to scrape only your own links, replace the list. Single-page links are also supported. A listing detail page (`/en/property/<slug>-<id>`), a project detail page (`/en/property/project/<slug>-<id>`, with a `unit_types` price ladder) and an agent profile page (`/en/agent/<slug>-<id>`) return one record each. The agent directory page (`/en/agent-directory`) returns one agent row per featured card (about 30 to 35, a rotating set), and the agent search page (`/en/agent-directory/search`) returns up to 20 agent rows of about 1,400 because the site ignores page and filter parameters. Every row counts against **Max properties** and is billed. Agent pages produce `agent` rows; the profile page masks the phone number, so none is returned there. Bare ddproperty.com links are rewritten to www, and links under /th/ are rejected because the site refuses them. In search mode the region field also accepts a plain name such as Bangkok.
3. Set **Max properties** to control run size and cost, and toggle **Fetch detail pages** for coordinates, description and facilities.
4. Click **Start**, then download the dataset as JSON, CSV or Excel, or read it through the API.

**Search for condos for sale in Bangkok:**

```json
{
  "mode": "search",
  "listing_type": "sale",
  "property_type": "CONDO",
  "region": "TH10",
  "max_properties": 20
}
```

**Rent search by free text location with price and bedroom filters:**

```json
{
  "mode": "search",
  "listing_type": "rent",
  "search": "Sukhumvit",
  "min_price": 15000,
  "max_price": 50000,
  "bedrooms": 1,
  "max_properties": 50
}
```

**Paste DDproperty search URLs:**

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

**Recurring monitoring of a Phuket sale search:**

```json
{
  "mode": "search",
  "listing_type": "sale",
  "region": "TH83",
  "incrementalMode": true,
  "max_properties": 0
}
```

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/ddproperty-scraper").call(run_input={"mode": "search", "listing_type": "sale", "region": "TH10"})
for listing in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(listing["title"], listing["price_formatted"])
```

JavaScript:

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

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('abotapi/ddproperty-scraper').call({ mode: 'search', listing_type: 'sale', region: 'TH10' });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

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

#### How `max_properties` shapes a run

The actor collects listings page by page in the order the site returns them, pushing each page as it goes, and stops once `max_properties` is reached or the search runs out of pages. Set it to `0` to collect every listing the search returns. Fetching detail pages for coordinates, description and facilities adds one extra request per listing, so a run with **Fetch detail pages** on takes longer than one with it off; **Detail page concurrency** controls how many of those detail fetches run in parallel (all of them share one connection, so values above 3 risk being rate-limited).

#### Resume and recurring updates

- **Resume** (`resumeFromRunId`) continues one interrupted run: paste its run or dataset ID and the actor skips every listing id it already collected there, so you don't pay twice.
- **Incremental mode** (`incrementalMode`) is for recurring runs over the same search. Each listing is classified `NEW`, `UPDATED` (with `changedFields`), `UNCHANGED` (suppressed and not billed unless `emitUnchanged` is on), `REAPPEARED`, or `EXPIRED` (only after a run that scanned its search to a genuine natural end, no `max_properties` cap hit and no Resume, and only with `emitExpired` on). `stateKey` names or shares the stored state; leave it blank and the actor derives the tracked scope from your search filters, so two different searches never share a baseline by accident. With incremental mode off, output is exactly as before. Tracking the same listing both through a search link and through its detail link under one `stateKey` counts the first switch as an update, because the detail page carries extra fields.
- **Ignore fields when detecting changes** (`ignoreFieldsForChanges`, incremental mode only) lists field names (at any depth, or a dotted path like `agent.rank`) that stay in your output but never make a listing `UPDATED` or show up in `changedFields`. Always excluded from change detection, still returned in the output: `recency`, a relative `posted_date` text such as "2 days ago", and the query string of image URLs (only the image path is compared). Everything else, including `badges`, price and status, counts as a change.

### 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 listing, 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.

### Input Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `mode` | string | (prefill: `search`) | `search` (build a URL from the filters below) or `url` (paste DDproperty search URLs). |
| `listing_type` | string | `sale` | `sale` or `rent`. Applies in search mode only. |
| `property_type` | string | (none) | `CONDO`, `BUNG`, `SEMI`, `VIL`, `TOWN`, `LAND`, `APT`, `RET`, `OFF`, `WAR`, `BIZ`, `SHOP`. Search mode only. |
| `search` | string | (none) | Free text location (e.g. "Sukhumvit"). Takes priority over `region` if both are set. Search mode only. |
| `region` | string | (none) | Region, district or area code (e.g. `TH10` = Bangkok, `TH83` = Phuket, `TH1033` = Watthana). Search mode only. |
| `min_price` | integer | (none) | Minimum price in Thai Baht. Search mode only. |
| `max_price` | integer | (none) | Maximum price in Thai Baht. Search mode only. |
| `bedrooms` | integer | (none) | Number of bedrooms, `0` = Studio. Search mode only. |
| `sort` | string | `date` | `date`, `price`, or `psf`. Search mode only. |
| `sort_order` | string | `desc` | `asc` or `desc`. Search mode only. |
| `urls` | array | (default: one example URL per supported shape; they share the Max properties budget, so the first fills the default 20) | One or more DDproperty search URLs. URL mode only; ignores the search-mode filters above. |
| `max_properties` | integer | `20` | Stop after this many listings (`0` = no limit). Caps both modes; the schema maximum is 100000. |
| `enable_detail_pages` | boolean | `true` | Fetch each listing's detail page for latitude/longitude, description, facilities and project info. |
| `detail_concurrency` | integer | `1` | Parallel detail-page fetches (1 to 5). |
| `session_ttl_seconds` | integer | `300` | How long the actor reuses one browsing session before refreshing it. |
| `resumeFromRunId` | string | (none) | Continue one interrupted run: paste its run or dataset ID. |
| `incrementalMode` | boolean | `false` | Return only new and changed listings on recurring runs of the same search. |
| `stateKey` | string | (none) | Name or share an incremental-mode state across runs. Leave blank to auto-derive it from your search filters. |
| `emitUnchanged` | boolean | `false` | Also return (and bill) unchanged listings in incremental mode. |
| `emitExpired` | boolean | `false` | Also return (and bill) listings that disappeared, in incremental mode. |
| `ignoreFieldsForChanges` | array | `[]` | Incremental mode only: field names that never count as a change (kept in the output). |
| `proxy` | object | (prefill: Apify Residential proxy, country TH) | Connection settings. |
| `startUrls` | string/array/integer | (none) | Alternative name for `urls`; switches to URL mode. See Alternative input names. |
| `searchQueries` | string/array/integer | (none) | Alternative: one search per entry. See Alternative input names. |
| `maxItems` | string/array/integer | (none) | Alternative name for `max_properties`. See Alternative input names. |
| `minPrice` | string/array/integer | (none) | Alternative name for `min_price`. See Alternative input names. |
| `maxPrice` | string/array/integer | (none) | Alternative name for `max_price`. See Alternative input names. |
| `listingType` | string/array/integer | (none) | Alternative name for `listing_type`. See Alternative input names. |
| `propertyType` | string/array/integer | (none) | Alternative property type names such as `th_condo`. See Alternative input names. |
| `country` | string/array/integer | (none) | Thailand only; `sg`, `my` or `vn` stop the run with a pointer to our sister scrapers. See Alternative input names. |
| `mcpConnectors` | array | (none) | Optional: send a summary of each listing to apps you authorized under Integrations. |
| `notionParentPageUrl` | string | (none) | Notion connector only: page under which listing pages are created. |
| `maxNotifyListings` | integer | `50` | Cap on items written to each connector per run. |

#### Alternative input names

If you come from another property scraper, these names work as-is and are folded into the inputs above (the alternative name wins when both are set): `startUrls` (switches to URL mode; strings or `{"url": ...}`), `searchQueries` (one search per entry; the cap is the total across all queries), `maxItems` = `max_properties`, `minPrice` / `maxPrice` = `min_price` / `max_price`, `listingType` = `listing_type`, and `propertyType` with `th_condo` = CONDO, `th_single_detached` = BUNG, `th_semi_detached` = SEMI, `th_villa` = VIL, `th_townhouse` = TOWN, `th_land` = LAND, `th_apartment` = APT (`auto` / `th_all` = no filter). This actor covers Thailand only: `country` sg, my or vn (and sg\_*, my\_*, vn\_\* property types) stop the run with a pointer to our sister Singapore / Malaysia / Vietnam scrapers.

#### Extra output fields

Every listing also carries `type` (`listing`, or `agent` for agent rows), `sourceSite` (`ddproperty.com`), `currency` (`THB`), `thumbnail` (first image), `origin` (the start URL or search query that produced it), `page` (search page number), `agentProfileUrl` and `videos` (when the site exposes them), plus search-card flags such as `isAgentVerified`, `isBoosted` and `buildYear` when present. `floorAreaSqm` is the same value as `floor_area` and `agentName` the same value as `agent_name`. These fields are additive and are not part of incremental change detection. Agent rows are additional rows: each agent row pushed is billed as a `result` event like a listing row (see Pricing).

### Output Example

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

```json
{
  "id": 12345678,
  "title": "Condo for Sale in Watthana, Bangkok",
  "address": "Sukhumvit Soi 24, Khlong Tan, Watthana, Bangkok",
  "listing_type": "sale",
  "property_type": "CONDO",
  "price": 8500000,
  "price_formatted": "฿8,500,000",
  "price_per_sqm": "฿141,667 / sqm",
  "bedrooms": 2,
  "bathrooms": 2,
  "floor_area": 60,
  "tenure": "Freehold",
  "developer": "Sample Developer Co.",
  "is_developer_listing": true,
  "agent_name": "Agent Name",
  "agent_id": 445566,
  "agency_id": 778899,
  "region": "Bangkok",
  "region_code": "TH10",
  "district": "Watthana",
  "area": "Khlong Tan",
  "latitude": 13.7305,
  "longitude": 100.5697,
  "location_source": "detail_page",
  "nearby_mrt": "7 min (560 m) from E5 Asok BTS",
  "nearby_station_id": "E5",
  "badges": ["Freehold", "Condo"],
  "posted_date": "07 Jul 2026",
  "posted_unix": 1783382400,
  "images": ["https://www.ddproperty.com/sample/image1.jpg"],
  "image_count": 1,
  "url": "https://www.ddproperty.com/en/property/sample-condo-12345678",
  "status": "ACT",
  "description": "Spacious condo close to Asok BTS, sample listing description.",
  "facilities": ["Swimming Pool", "Fitness Centre"]
}
```

### Plan Requirement

The site sits behind a managed browser challenge that blocks plain requests on every connection type, so the actor opens a browsing session first and then reuses it. Keep **Connection** on its prefilled setting (Apify residential proxy, country Thailand) or supply your own proxy URLs. Apify free plans do not include residential proxies: on a free plan the actor 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." and continues on a backup connection, so results on a free plan are not guaranteed. A paid Apify plan is recommended. We have not confirmed that datacenter proxies work.

### FAQ

#### How much does it cost?

You pay a `result` event for every row pushed to the dataset (listing rows and agent rows from agent profile, directory and search links; unchanged rows suppressed in incremental mode are not pushed and not billed), plus the platform's Actor Start event, charged per GB of the run's memory. The **Pricing** tab shows the current rates. Use **Max properties** to cap the cost of any run.

#### Is it legal to scrape DDproperty?

This actor collects only publicly available real estate listing data. You are responsible for how you use it: follow DDproperty's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution. Listing photos and agent or developer names may be subject to third-party rights.

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

Yes. Schedule the actor from the **Schedules** tab and turn on **Incremental mode**. Each run then returns only new, updated and reappeared listings for that search (plus expired ones with **Emit expired** on), and unchanged ones are not billed unless you turn on **Emit unchanged**.

#### What's the difference between Resume and Incremental mode?

**Resume** (`resumeFromRunId`) continues one specific interrupted run by its run or dataset ID; it is a one-off continuation. **Incremental mode** remembers a recurring search itself, across scheduled runs, with no ID to paste, and classifies every listing as new, updated, unchanged, reappeared or expired. They solve different problems and can be combined to bootstrap incremental tracking from an existing run.

#### Why did my run return fewer listings than expected, or none at all?

The run fails with a clear error message if it cannot establish or refresh a working browsing session, if URL mode is used with no URLs or with a URL that is not a DDproperty link, if a link is under `/th/` (the site refuses the Thai-locale path; use `/en/`), or if a pasted Resume ID cannot be read. If the proxy is refused for your account the run ends with "The proxy connection was refused for this account, so no page could be read"; if every results page fails to load it ends with "Could not load any results page (the requests failed), so nothing was collected. This is a connection problem, not an empty search." If the session is established but a search page later comes back blocked or empty, the actor stops collecting from that search and still finishes successfully, since it cannot tell a temporary block apart from a genuinely short result set. If you expected more listings than you got, check the run log for warnings before assuming there really were none.

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

Yes. Call it from any Apify integration or MCP client, and use the connector field to push results into Notion, Linear or Airtable.

### 🔗 Want more real estate data?

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

<table>
<tr><td>🏠 <a href="https://apify.com/abotapi/propertyguru-sg-scraper"><b>PropertyGuru SG Scraper</b></a><br>From $0.80/1K. Scrape PropertyGuru.com.sg sale and rental listings with 30+ structured...</td><td>🏠 <a href="https://apify.com/abotapi/commercialguru-sg-scraper"><b>CommercialGuru SG Property Listings &amp; Agent Contacts Scraper</b></a><br>Scrape commercialguru.com.sg Singapore listings at scale. Extract prices, PSF, floor...</td></tr>
<tr><td>🏠 <a href="https://apify.com/abotapi/propertyguru-my-scraper"><b>PropertyGuru MY Scraper</b></a><br>Scrape PropertyGuru.com.my sale and rental listings with 30+ structured fields, including...</td><td>🏨 <a href="https://apify.com/abotapi/hipflat-scraper"><b>Hipflat Scraper Thailand Property &amp; Projects</b></a><br>Scrape hipflat.co.th property listings and new-project data across Thailand: price, price...</td></tr>
<tr><td>🏠 <a href="https://apify.com/abotapi/iproperty-com-my-scraper"><b>iProperty MY Scraper</b></a><br>Scrape property listings from iProperty.com.my for sale or rent: price, PSF, bedrooms...</td><td>🏠 <a href="https://apify.com/abotapi/591-com-tw-scraper"><b>591.com.tw Scraper</b></a><br>Extract property listings from 591房屋交易網 591.com.tw, Taiwan’s largest real estate...</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/ddproperty-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 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 links. Open the site in a browser, refine your search with any filters, then paste the resulting URLs here. Listing-page links (region, district, near-station, property-type, commercial, filtered/sorted, new-launch) are walked page by page: pagination auto-advances forward from each URL, and a ?page=N start page is honoured. Single-page links: a listing detail page, a project detail page (with its unit-type price ladder) and an agent profile page return one record each; the agent directory page returns one agent row per featured card (about 30 to 35, a rotating set) and the agent search page up to 20 agent rows. Every row counts against Max properties and is billed. In URL mode, the search-mode fields above are ignored. Bare ddproperty.com links are normalised to www. The /th/ locale is refused by the site and rejected up front. The prefilled list has one example link per shape, but all links share the Max properties budget (default 20), so the first link fills it; raise Max properties or remove links to reach the other shapes.

## `startUrls` (type: `array`):

ddproperty.com search URLs. Same as "Search URLs (URL mode)"; setting this switches the run to URL mode. Plain strings or {"url": ...} objects are accepted.

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

Free-text location queries such as "Sukhumvit" or "Phuket". Each entry runs as its own search with the filters above; Max items / Max properties is the TOTAL cap across all queries.

## `country` (type: `string`):

Only "th" (Thailand) is served by this actor. sg, my and vn are accepted for compatibility but the run stops with a pointer to the sister scrapers.

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

sale or rent. Same value as "Listing type (Search mode)".

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

th\_\* values map to the property_type codes: th_condo=CONDO, th_single_detached=BUNG, th_semi_detached=SEMI, th_villa=VIL, th_townhouse=TOWN, th_land=LAND, th_apartment=APT. auto and th_all apply no type filter. sg\_*, my\_* and vn\_\* values are out of scope and stop the run.

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

Same value as "Min price (THB)".

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

Same value as "Max price (THB)".

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

Same value as "Max properties" (0 = unlimited). Total cap across all URLs and queries.

## `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. Hard upper bound 100000.

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

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

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

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

Incremental mode only. Field names (e.g. price_formatted, badges) that must NOT count as a change: they stay in the output but never make a listing UPDATED or appear in changedFields. Matches the name at any depth. Already ignored automatically: recency, a relative posted_date text ("2 days ago"), and query strings on image URLs. Leave empty to detect changes on every other field.

## `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",
    "https://www.ddproperty.com/en/property-for-rent/in-bangkok-th10",
    "https://www.ddproperty.com/en/property-for-sale",
    "https://www.ddproperty.com/en/property-for-sale/in-bangkok-th10?page=2",
    "https://www.ddproperty.com/en/property-for-sale?freetext=sukhumvit&propertyTypeCode=CONDO&minprice=2000000&bedrooms=2&sort=price&order=asc",
    "https://www.ddproperty.com/en/property-for-sale/in-bangkok-th10/priced-under-1m-thb",
    "https://www.ddproperty.com/en/property-for-sale/in-bang-bon-th1050",
    "https://www.ddproperty.com/en/property-for-sale/near-e9-on-nut-bts-2174",
    "https://www.ddproperty.com/en/condo-for-sale",
    "https://www.ddproperty.com/en/detached-house-for-sale",
    "https://www.ddproperty.com/en/commercial-property-for-sale",
    "https://www.ddproperty.com/en/new-property-launch?listingType=sale&page=1",
    "https://www.ddproperty.com/en/property/ideo-mobi-rama-9-bangkok-for-sale-500188725",
    "https://www.ddproperty.com/en/property/project/chapter-one-spark-charan-bangkok-for-sale-500444048",
    "https://www.ddproperty.com/en/agent/aksarapak-chittara-1265048",
    "https://www.ddproperty.com/en/agent-directory",
    "https://www.ddproperty.com/en/condo-for-sale/in-bangkok-th10",
    "https://www.ddproperty.com/en/condo-for-sale?freetext=Sukhumvit",
    "https://www.ddproperty.com/en/property-for-sale?freetext=Sukhumvit",
    "https://www.ddproperty.com/en/new-property-launch?region_code=TH10"
  ],
  "country": "th",
  "max_properties": 20,
  "enable_detail_pages": true,
  "detail_concurrency": 3,
  "session_ttl_seconds": 300,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "TH"
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "ignoreFieldsForChanges": [],
  "maxNotifyListings": 50
}
```

# Actor output Schema

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

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

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

Compact listing table.

## `map_view` (type: `string`):

Listings with coordinates and nearby transit.

## `agents` (type: `string`):

Agent and developer attribution per listing.

## `detailed` (type: `string`):

Detail-page enrichment: description, facilities, floor plans, media.

## `project_detail` (type: `string`):

New-launch project page rows with unit-type price ladder.

## `agent_records` (type: `string`):

Agent profile and agent directory rows.

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

Incremental-mode change rows (NEW, UPDATED, EXPIRED ...).

## `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",
        "https://www.ddproperty.com/en/property-for-rent/in-bangkok-th10",
        "https://www.ddproperty.com/en/property-for-sale",
        "https://www.ddproperty.com/en/property-for-sale/in-bangkok-th10?page=2",
        "https://www.ddproperty.com/en/property-for-sale?freetext=sukhumvit&propertyTypeCode=CONDO&minprice=2000000&bedrooms=2&sort=price&order=asc",
        "https://www.ddproperty.com/en/property-for-sale/in-bangkok-th10/priced-under-1m-thb",
        "https://www.ddproperty.com/en/property-for-sale/in-bang-bon-th1050",
        "https://www.ddproperty.com/en/property-for-sale/near-e9-on-nut-bts-2174",
        "https://www.ddproperty.com/en/condo-for-sale",
        "https://www.ddproperty.com/en/detached-house-for-sale",
        "https://www.ddproperty.com/en/commercial-property-for-sale",
        "https://www.ddproperty.com/en/new-property-launch?listingType=sale&page=1",
        "https://www.ddproperty.com/en/property/ideo-mobi-rama-9-bangkok-for-sale-500188725",
        "https://www.ddproperty.com/en/property/project/chapter-one-spark-charan-bangkok-for-sale-500444048",
        "https://www.ddproperty.com/en/agent/aksarapak-chittara-1265048",
        "https://www.ddproperty.com/en/agent-directory",
        "https://www.ddproperty.com/en/condo-for-sale/in-bangkok-th10",
        "https://www.ddproperty.com/en/condo-for-sale?freetext=Sukhumvit",
        "https://www.ddproperty.com/en/property-for-sale?freetext=Sukhumvit",
        "https://www.ddproperty.com/en/new-property-launch?region_code=TH10"
    ],
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "TH"
    },
    "ignoreFieldsForChanges": []
};

// 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",
        "https://www.ddproperty.com/en/property-for-rent/in-bangkok-th10",
        "https://www.ddproperty.com/en/property-for-sale",
        "https://www.ddproperty.com/en/property-for-sale/in-bangkok-th10?page=2",
        "https://www.ddproperty.com/en/property-for-sale?freetext=sukhumvit&propertyTypeCode=CONDO&minprice=2000000&bedrooms=2&sort=price&order=asc",
        "https://www.ddproperty.com/en/property-for-sale/in-bangkok-th10/priced-under-1m-thb",
        "https://www.ddproperty.com/en/property-for-sale/in-bang-bon-th1050",
        "https://www.ddproperty.com/en/property-for-sale/near-e9-on-nut-bts-2174",
        "https://www.ddproperty.com/en/condo-for-sale",
        "https://www.ddproperty.com/en/detached-house-for-sale",
        "https://www.ddproperty.com/en/commercial-property-for-sale",
        "https://www.ddproperty.com/en/new-property-launch?listingType=sale&page=1",
        "https://www.ddproperty.com/en/property/ideo-mobi-rama-9-bangkok-for-sale-500188725",
        "https://www.ddproperty.com/en/property/project/chapter-one-spark-charan-bangkok-for-sale-500444048",
        "https://www.ddproperty.com/en/agent/aksarapak-chittara-1265048",
        "https://www.ddproperty.com/en/agent-directory",
        "https://www.ddproperty.com/en/condo-for-sale/in-bangkok-th10",
        "https://www.ddproperty.com/en/condo-for-sale?freetext=Sukhumvit",
        "https://www.ddproperty.com/en/property-for-sale?freetext=Sukhumvit",
        "https://www.ddproperty.com/en/new-property-launch?region_code=TH10",
    ],
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "TH",
    },
    "ignoreFieldsForChanges": [],
}

# 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",
    "https://www.ddproperty.com/en/property-for-rent/in-bangkok-th10",
    "https://www.ddproperty.com/en/property-for-sale",
    "https://www.ddproperty.com/en/property-for-sale/in-bangkok-th10?page=2",
    "https://www.ddproperty.com/en/property-for-sale?freetext=sukhumvit&propertyTypeCode=CONDO&minprice=2000000&bedrooms=2&sort=price&order=asc",
    "https://www.ddproperty.com/en/property-for-sale/in-bangkok-th10/priced-under-1m-thb",
    "https://www.ddproperty.com/en/property-for-sale/in-bang-bon-th1050",
    "https://www.ddproperty.com/en/property-for-sale/near-e9-on-nut-bts-2174",
    "https://www.ddproperty.com/en/condo-for-sale",
    "https://www.ddproperty.com/en/detached-house-for-sale",
    "https://www.ddproperty.com/en/commercial-property-for-sale",
    "https://www.ddproperty.com/en/new-property-launch?listingType=sale&page=1",
    "https://www.ddproperty.com/en/property/ideo-mobi-rama-9-bangkok-for-sale-500188725",
    "https://www.ddproperty.com/en/property/project/chapter-one-spark-charan-bangkok-for-sale-500444048",
    "https://www.ddproperty.com/en/agent/aksarapak-chittara-1265048",
    "https://www.ddproperty.com/en/agent-directory",
    "https://www.ddproperty.com/en/condo-for-sale/in-bangkok-th10",
    "https://www.ddproperty.com/en/condo-for-sale?freetext=Sukhumvit",
    "https://www.ddproperty.com/en/property-for-sale?freetext=Sukhumvit",
    "https://www.ddproperty.com/en/new-property-launch?region_code=TH10"
  ],
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "TH"
  },
  "ignoreFieldsForChanges": []
}' |
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/1Dmu82V1KYYJOrpOF/openapi.json
