# PropertyGuru MY Scraper - Properties, Prices & Agents (`abotapi/propertyguru-my-scraper`) Actor

Scrape PropertyGuru.com.my sale and rental listings with 30+ structured fields, including MYR price, built-up area, tenure, nearby transit, property details, agent information and GPS coordinates. Fast and reliable at scale.

- **URL**: https://apify.com/abotapi/propertyguru-my-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Categories:** Real estate, Automation, Developer tools
- **Features:** Limited permissions
- **Stats:** 18 total users, 3 monthly users, 70.5% 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

## PropertyGuru Malaysia Scraper: Sale & Rent Listings with Price, Coordinates & Agent Data

PropertyGuru Malaysia Scraper turns propertyguru.com.my into structured data. Get sale and rent listings with price in MYR, built up and land area, tenure, nearest transit, agent and agency details, and GPS coordinates. Search by state or free text location, filter by property type, price and bedrooms, or paste a search URL you already built in the browser, then export to JSON, CSV or Excel, or pull the results straight into your app through the API.

### Why This Scraper?

- **Two ways to search.** Pick a state or federal territory from the dropdown, or type a free text location like "Mont Kiara" or "Bukit Bintang". Paste your own propertyguru.com.my search URL instead if you need filters the built in fields don't cover.
- **Condo, landed, commercial and land.** Filter by property type on top of listing type (sale or rent), price range and bedroom count. "Landed" narrows to terrace houses only; for bungalows, semi-D or cluster homes, use URL mode instead.
- **Detail page enrichment on demand.** Turn on detail pages to add coordinates, full description, project name, total units, facilities, floor plans and nearby schools to every listing.
- **Project level caching.** For condos, the actor fetches one unit's detail page per project and reuses the shared project fields (facilities, coordinates, developer, total units) across every other unit in the same development, cutting the cost of condo heavy runs.
- **All 13 states plus the 3 federal territories.** A single dropdown covers every Malaysian state and federal territory, so state wide monitoring needs no manual URL building.
- **Built for schedules.** Incremental mode returns only new and changed listings on recurring runs, and a resume option continues one interrupted crawl without re paying for listings you already collected.
- **Bounded access retries.** If access is refused, the actor retries through its configured connections and reports a clear retryable failure when access remains blocked.

### Use Cases

- **Property portals and listing aggregators:** pull structured MYR pricing, area and location data to power a search or comparison site.
- **Investment and market research:** track price per square foot, tenure and bedroom mix across states to spot undervalued areas.
- **Lead generation for agents and agencies:** collect agent names, licenses and contact details alongside the listings they represent.
- **Recurring monitoring:** schedule the actor on a saved search (a state, a project, a price band) and get only new, updated or reappeared listings on each run.
- **Mapping and location apps:** feed latitude and longitude, nearest transit and nearby schools into a map based property tool.

### Data You Get

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

| Field | Example |
| --- | --- |
| `id` | `36124001` |
| `title` | `"Mont Kiara Pelangi"` |
| `address` | `"Jalan Kiara, Mont Kiara, Kuala Lumpur"` |
| `url` | `"https://www.propertyguru.com.my/property-listing/mont-kiara-pelangi-36124001"` |
| `status` | `"active"` |
| `listing_type` | `"sale"` (also `"rent"`) |
| `property_type` | `"Condo"` (also `"Landed"`, `"Commercial"`) |
| `price` / `price_formatted` | `680000` / `"RM 680,000"` |
| `currency` | `"MYR"` |
| `price_psf` | `"RM 680 psf"` |
| `bedrooms` / `bathrooms` | `3` / `2` |
| `floor_area` | `1000` |
| `land_area` | `1800` (landed listings) |
| `tenure` | `"Freehold"` |
| `state` / `region` / `city` | `"Kuala Lumpur"` / `"Mont Kiara"` / `"Kuala Lumpur"` |
| `nearby_mrt` | `"5 min (450 m) to Sri Hartamas MRT"` |
| `agent_name` / `agent_license` | `"Kevin Chai"` / `"PEA1234"` |
| `agency_name` | `"ABC Realty Sdn Bhd"` |
| `developer` / `is_developer_listing` | `"Sample Developer Bhd"` / `false` |
| `posted_date` / `recency` | `"30 Jul 2026"` / `"Listed 8m ago"` |
| `images` / `image_count` | array of photo URLs / `8` |
| `latitude` / `longitude` | `3.1698` / `101.6509` (with detail pages on) |
| `project_name` / `total_units` | `"Mont Kiara Pelangi"` / `545` (with detail pages on) |

When detail pages are switched on, listings also carry `description`, `headline`, `facilities`, `nearby_schools`, `floor_plans`, `unit_features`, `furnishing`, `unit_floor`, `virtual_tour_url`, `top_year`, `postal_code`, `reference_number`, `agent_mobile`, `agent_job_title` and `agency_license`, all folded straight into the same listing record rather than a separate object. Fields the site does not expose for a given listing are simply left out of that record rather than filled with a placeholder.

A new-launch/project collection URL (see the URL mode table below) returns one row per development rather than per unit, so `bedrooms`/`bathrooms` on those rows are the project's full configuration range as text (e.g. `"2, 3"`) rather than a single number, and `price_formatted` is often prose like `"Price on ask"` when the developer hasn't published a fixed price.

### How to Use

1. Choose **Search mode** (build a URL from state and filters) or **URL mode** (paste your own propertyguru.com.my search URLs).
2. In search mode, pick listing type, property type, a state or a free text location, and any of price, bedrooms and sort.
3. Turn on **Fetch Detail Pages** if you want coordinates, description, project facts and schools added to every listing.
4. Set **Max Properties** and **Max Pages** to control run size and cost, then click **Start**.

**All condos for sale in Kuala Lumpur:**

```json
{
  "mode": "search",
  "listing_type": "sale",
  "property_type": "condo",
  "state": "kuala-lumpur",
  "max_properties": 20,
  "max_pages": 5
}
```

**Rentals near a specific neighbourhood:**

```json
{
  "mode": "search",
  "listing_type": "rent",
  "search": "Mont Kiara",
  "bedrooms": 3,
  "enable_detail_pages": true
}
```

**Terrace houses in Selangor under a price ceiling:**

```json
{
  "mode": "search",
  "listing_type": "sale",
  "property_type": "landed",
  "state": "selangor",
  "max_price": 1500000,
  "sort": "price",
  "sort_order": "asc"
}
```

**A pre built search URL:**

```json
{
  "mode": "url",
  "urls": ["https://www.propertyguru.com.my/condo-for-sale?freetext=Bangsar&bedrooms=3"],
  "max_pages": 10
}
```

#### URL mode: every supported link shape

Paste any of these propertyguru.com.my URL shapes into `urls`. An unrecognized shape (for example,
there is no public per-agency profile page on this site) logs a loud warning in the run log instead of
silently returning 0 items.

| Shape | Example | What you get |
|---|---|---|
| Search results | `/property-for-sale?freetext=Kuala+Lumpur` | Every listing on the search, paginated |
| Category path | `/condo-for-sale`, `/bungalow-for-rent`, `/shops-for-sale`, `/residential-land-for-sale` | Same as search, scoped to that property type |
| Location path | `/condo-for-sale/in-kuala-lumpur-58jok` | Same as search, scoped to that city/region slug |
| Paginated start | `/property-for-sale/2?freetext=Kuala+Lumpur` | Resumes from page 2 onward (page 1 is never walked, so this scope can't confirm EXPIRED listings in incremental mode) |
| New launch / project collection | `/new-property-launch?region_code=58jok` | Every project card on the collection (project name, developer, price range, tenure) |
| Single listing detail | `/property-listing/<slug>-<id>` | That one listing, matched on its stable id |
| Project detail | `/property-listing/project/<slug>-<id>` | That one project, matched on its stable id |
| Agent profile | `/agent/<slug>-<id>` | Resolved to that agent's own active sale + rent listings |

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/propertyguru-my-scraper").call(run_input={
    "mode": "search",
    "listing_type": "sale",
    "state": "kuala-lumpur",
    "max_properties": 20,
})
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/propertyguru-my-scraper').call({
  mode: 'search',
  listing_type: 'sale',
  state: 'kuala-lumpur',
  max_properties: 20,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

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

#### Resume and recurring updates

- **Resume** (`resumeFromRunId`) continues one interrupted crawl. Paste its run or dataset ID and this run skips every listing ID already collected there, so you don't pay twice.
- **Incremental mode** (`incrementalMode`) is for scheduling the same search or URL setup repeatedly, for example daily. State is kept separately per search (mode, state, listing type, property type, bedrooms, price range, sort, or your own URLs), or share it deliberately with your own `stateKey`. Each listing is classified `NEW`, `UPDATED` (with `changedFields`), `UNCHANGED` (suppressed and not billed unless `emitUnchanged` is on), `REAPPEARED` (a listing that was previously marked gone and is now found again) or `EXPIRED`. `EXPIRED` rows, gated behind `emitExpired`, are only produced once a run reaches the natural end of the tracked search, meaning it was not cut short by Max Properties, Max Pages, a blocked page, or a resumed run. If a run was capped or blocked before reaching the end, the previous state for those listings is left as is and no `EXPIRED` rows are emitted that run. With incremental mode off, output is exactly as a normal run.

### 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 | `search` | `search` (build a URL from the filters below) or `url` (use provided URLs). |
| `urls` | array | none (prefill: one sample Kuala Lumpur search URL) | Search or listing URLs to scrape. URL mode only; ignored in search mode. |
| `listing_type` | string | `sale` | `sale` or `rent`. Search mode only. |
| `property_type` | string | none | `condo` (condos and apartments), `landed` (terrace houses only), `commercial` (shop, office, factory), or `land` (residential land). For bungalows, semi-D or cluster homes, use URL mode. |
| `state` | string | none | Malaysian state or federal territory. Use this or `search`, not both. |
| `search` | string | none | Free text location, for example "Mont Kiara" or "Bangsar". Use this or `state`, not both. |
| `min_price` | integer | none | Minimum price in MYR. |
| `max_price` | integer | none | Maximum price in MYR. |
| `bedrooms` | integer | none | Number of bedrooms (`0` = studio). |
| `sort` | string | `date` | Sort by `date`, `price`, or `psf`. |
| `sort_order` | string | `desc` | `asc` or `desc`. |
| `enable_detail_pages` | boolean | `true` | Fetch each listing's detail page for coordinates, description, project name, total units, facilities, floor plans and nearby schools. |
| `max_properties` | integer | `10` | Maximum number of listings to scrape (`0` = unlimited). |
| `max_pages` | integer | `20` | Maximum number of search result pages to walk (20 listings per page). |
| `proxy` | object | Apify Residential, Malaysia | Connection settings. Residential proxy in Malaysia is required because the site is geo restricted to Malaysian IPs. |
| `detail_concurrency` | integer | `1` | Internal: number of parallel detail page fetches. Hidden from the input form; still accepted via the API so existing saved inputs and schedules keep working. |
| `session_ttl_seconds` | integer | `600` | Internal: how often the scraper refreshes its connection state. Hidden from the input form; still accepted via the API so existing saved inputs and schedules keep working. |
| `reuse_project_cache` | boolean | `true` | Internal: reuse cached project level fields (project name, total units, facilities, schools, coordinates, developer, tenure) across listings in the same condo project instead of fetching each one's detail page. Hidden from the input form; still accepted via the API so existing saved inputs and schedules keep working. |
| `dataset_name` | string | none | Internal: custom dataset name. Leave empty for the default dataset. Hidden from the input form; still accepted via the API so existing saved inputs and schedules keep working. |
| `clear_dataset` | boolean | `false` | Internal: clear the dataset before scraping. Hidden from the input form; still accepted via the API so existing saved inputs and schedules keep working. |
| `resumeFromRunId` | string | none | Continue one interrupted run without re-collecting or re-charging listings already in it. |
| `incrementalMode` | boolean | `false` | Track this same search, filter or URL setup run to run and return only NEW, UPDATED and REAPPEARED listings by default. |
| `stateKey` | string | none | Name or share an incremental mode monitoring campaign. Left empty, the actor derives a key automatically from your search and filter settings. |
| `emitUnchanged` | boolean | `false` | Incremental mode only. Also return, and bill, listings unchanged since the last run. |
| `emitExpired` | boolean | `false` | Incremental mode only. Also return, and bill, listings no longer found, once a run fully scans the tracked search. |
| `ignoreFieldsForChanges` | array | none | Incremental mode only: field names that should NOT make a listing count as `UPDATED`. `recency` is always ignored (it changes on every fetch without the listing actually changing). Use a bare field name to ignore it anywhere, or a dotted path (e.g. `nearby_mrts.name`) for one exact nested field. Ignored fields are still returned in the output. |
| `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 listings are created. |
| `maxNotifyListings` | integer | `50` | Cap on items written to each connector per run. |

### Output Example

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

```json
{
  "id": 36124001,
  "title": "Mont Kiara Pelangi",
  "address": "Jalan Kiara, Mont Kiara, Kuala Lumpur",
  "listing_type": "sale",
  "property_type": "Condo",
  "price": 680000,
  "price_formatted": "RM 680,000",
  "currency": "MYR",
  "price_psf": "RM 680 psf",
  "bedrooms": 3,
  "bathrooms": 2,
  "floor_area": 1000,
  "tenure": "Freehold",
  "state": "Kuala Lumpur",
  "city": "Kuala Lumpur",
  "nearby_mrt": "5 min (450 m) to Sri Hartamas MRT",
  "agent_name": "Kevin Chai",
  "agent_license": "PEA1234",
  "agency_name": "ABC Realty Sdn Bhd",
  "posted_date": "30 Jul 2026",
  "images": ["https://my1-cdn.pgimgs.com/listing/36124001/UPHO.157827896.V800/a.jpg"],
  "image_count": 8,
  "url": "https://www.propertyguru.com.my/property-listing/mont-kiara-pelangi-36124001",
  "status": "active",
  "latitude": 3.1698,
  "longitude": 101.6509,
  "project_name": "Mont Kiara Pelangi",
  "total_units": 545,
  "facilities": ["Swimming Pool", "Gymnasium", "24-hour Security"]
}
```

### Plan Requirement

The default proxy setting (Apify Residential, Malaysia) works out of the box, since propertyguru.com.my is geo restricted to Malaysian IPs and needs a Malaysian residential connection either way. The actor uses bounded access retries. If all configured connection attempts are blocked, the run fails with a retry message and preserves any results already collected. Check the **Pricing** tab for the current rates.

### FAQ

#### How much does it cost?

You pay per listing returned, with detail page enrichment billed only for listings whose detail page was actually fetched. The **Pricing** tab shows the current rates. Use **Max Properties** and **Max Pages** to cap the cost of any run.

#### Is it legal to scrape PropertyGuru?

This actor collects only publicly available property listing data. You are responsible for how you use it: follow PropertyGuru's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution. Listing facts such as price and area are generally not protected, but photos and some descriptive text 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, and unchanged ones are not billed unless you turn on **Emit unchanged listings**.

#### Why does incremental mode sometimes return nothing?

If nothing changed since the last run, that is the expected result: incremental mode suppresses `UNCHANGED` listings by default, so a quiet run legitimately returns zero rows while still recording that it scanned everything.

#### Why did my run fail or return nothing?

If the site refuses access, the actor uses its bounded access retries and then stops with a clear "please retry" message if access remains blocked. Run it again in a few minutes. A `resumeFromRunId` value that does not point to a valid run or dataset in your account also fails the run immediately, before anything is scraped or charged.

#### 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 PropertyGuru 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/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></tr>
<tr><td>🏠 <a href="https://apify.com/abotapi/ddproperty-scraper"><b>DDproperty Thailand Scraper</b></a><br>Scrape DDproperty.com Thailand listings at scale. Extract prices, property features...</td><td>🏷️ <a href="https://apify.com/abotapi/mudah-my-scraper"><b>Mudah.my Cars, Property, Jobs &amp; Marketplace Scraper</b></a><br>Scrape Mudah.my listings across every category: cars, motorcycles, property, mobiles...</td></tr>
<tr><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><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>
</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/propertyguru-my-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 state/search/filters below. URL mode = paste any propertyguru.com.my search URL already refined in a browser (recommended for complex filters).

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

URLs to scrape. Supports a search results page, a category path (e.g. /condo-for-sale), a location path (e.g. /condo-for-sale/in-kuala-lumpur-58jok), a new-launch/project collection page, a single listing detail page, an agent profile page (resolved to that agent's own sale + rent listings), and a paginated start (e.g. /property-for-sale/2?...). Tip: open propertyguru.com.my in your browser, apply filters, then paste the resulting URL here. An unsupported URL shape logs a loud warning instead of silently returning 0 items. In URL mode, all the filter fields below are ignored.

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

Type of listing to search for. Search mode only.

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

Filter by property type. 'condo' covers condominiums and apartments; 'landed' narrows to terrace houses (the most common landed sub-type; for bungalow/semi-D/cluster use a free-text location instead); 'commercial' covers shop/office/factory; 'land' covers residential land plots.

## `state` (type: `string`):

Malaysian state or federal territory. Pick this OR the free-text location, not both. Use 'Kuala Lumpur' for KL, 'Selangor' for the surrounding state.

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

Free-text location search (e.g. 'Mont Kiara', 'Bangsar', 'Cyberjaya', 'Bukit Bintang', 'Iskandar Puteri'). Use this OR the state dropdown, not both.

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

Minimum price in Malaysian Ringgit

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

Maximum price in Malaysian Ringgit

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

Number of bedrooms (0=Studio)

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

Sort results by date (newest), price, or price per sqft

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

Sort direction

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

Fetch each listing's detail page for richer attributes: latitude/longitude, full description, project name, total units, facilities, floor plans, and nearby schools. Recommended. Applies regardless of search mode (Search or URL).

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

Maximum number of properties to scrape (0=unlimited)

## `max_pages` (type: `integer`):

Maximum number of search result pages to scrape (20 listings per page)

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

Use Apify Residential in Malaysia for the default connection. PropertyGuru is geo-restricted to MY IPs. If the site remains unreachable after retries, the run stops with a retry message. Detail-page enrichment follows your Fetch Detail Pages setting.

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

Internal: number of parallel detail-page fetches. Picking a safe rate is the scraper's job, not the user's; higher values are faster but may trigger rate limiting. Hidden from the form; still accepted from the API so existing saved inputs and schedules keep working unchanged.

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

Internal: how often the scraper refreshes its connection state. The default of 10 minutes comfortably covers typical runs. Hidden from the form; still accepted from the API so existing saved inputs and schedules keep working unchanged.

## `reuse_project_cache` (type: `boolean`):

Internal: when enabled, the scraper caches project-level fields (project_name, total_units, facilities, nearby_schools, coordinates, developer, tenure) from the first listing in each condo project and reuses them on subsequent listings in the same project. Every listing is still tracked accurately; per-listing fields such as price and description come from the listing card. Hidden from the form; still accepted from the API so existing saved inputs and schedules keep working unchanged.

## `dataset_name` (type: `string`):

Internal: custom dataset name (empty = the default dataset). Hidden from the form; still accepted from the API so existing saved inputs and schedules keep working unchanged.

## `clear_dataset` (type: `boolean`):

Internal: clear the dataset before scraping. Hidden from the form; still accepted from the API so existing saved inputs and schedules keep working unchanged.

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

Paste a previous run ID or dataset ID to continue a large crawl of listings without returning or charging for listings already collected there. Use this after an interrupted run, or when continuing a search pull in another run. For recurring daily monitoring of the same search, use Incremental mode below instead.

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

Turn this on for daily or recurring monitoring. The first run returns all matching listings as NEW. Later runs normally return only NEW, UPDATED, and REAPPEARED listings. Turn on "Emit unchanged" or "Emit expired" only when you also want those listings returned (and billed). State is kept separately for each search/filter/URL setup; use State key when you want to name or deliberately share a monitoring campaign. To continue one specific interrupted run instead, use Resume from a previous run above.

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

Optional. Name this monitoring campaign to keep its state stable, or to deliberately share state across differently-configured runs. Leave empty to let the actor derive a key automatically from the search/filter/URL settings — different searches then never mix state with each other.

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

Off by default. Turn on to also return listings that have not changed since the last run, marked UNCHANGED. This returns — and bills — extra rows you already have, so leave it off unless you specifically want the full snapshot every run.

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

Off by default. Turn on to also return listings that were present in a previous run but are no longer found, marked EXPIRED. Only produced once a run has fully scanned the tracked search — not when Max pages/properties capped it, a page was blocked, or Resume was used. This returns — and bills — extra synthetic rows, so leave it off unless you need expiry tracking.

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

Output field names that should NOT make a listing count as UPDATED. `recency` is always ignored, because it is a relative-age render ("8m ago") that changes on every fetch even when nothing else about the listing does. For a field nested inside a list like nearby_mrts/nearby_schools, use a dotted path, e.g. nearby_schools.distance_km, to ignore it within every entry of that list rather than the whole list at once. Ignored fields are still returned in the output — they just never trigger an UPDATED row or appear in changedFields by themselves.

## `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",
  "urls": [
    "https://www.propertyguru.com.my/property-for-sale?freetext=Kuala+Lumpur",
    "https://www.propertyguru.com.my/property-for-rent?freetext=Selangor",
    "https://www.propertyguru.com.my/property-for-sale/2?freetext=Kuala+Lumpur",
    "https://www.propertyguru.com.my/condo-for-sale",
    "https://www.propertyguru.com.my/condo-for-sale/in-kuala-lumpur-58jok",
    "https://www.propertyguru.com.my/new-property-launch?region_code=58jok",
    "https://www.propertyguru.com.my/property-listing/kuala-lumpur-kuala-lumpur-kuala-lumpur-for-sale-by-kane-chai-36124001",
    "https://www.propertyguru.com.my/agent/kane-chai-12690529"
  ],
  "listing_type": "sale",
  "sort": "date",
  "sort_order": "desc",
  "enable_detail_pages": true,
  "max_properties": 10,
  "max_pages": 20,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "MY"
  },
  "detail_concurrency": 1,
  "session_ttl_seconds": 600,
  "reuse_project_cache": true,
  "clear_dataset": false,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "maxNotifyListings": 50
}
```

# Actor output Schema

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

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

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

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

# 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 = {
    "urls": [
        "https://www.propertyguru.com.my/property-for-sale?freetext=Kuala+Lumpur",
        "https://www.propertyguru.com.my/property-for-rent?freetext=Selangor",
        "https://www.propertyguru.com.my/property-for-sale/2?freetext=Kuala+Lumpur",
        "https://www.propertyguru.com.my/condo-for-sale",
        "https://www.propertyguru.com.my/condo-for-sale/in-kuala-lumpur-58jok",
        "https://www.propertyguru.com.my/new-property-launch?region_code=58jok",
        "https://www.propertyguru.com.my/property-listing/kuala-lumpur-kuala-lumpur-kuala-lumpur-for-sale-by-kane-chai-36124001",
        "https://www.propertyguru.com.my/agent/kane-chai-12690529"
    ],
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "MY"
    },
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/propertyguru-my-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 = {
    "urls": [
        "https://www.propertyguru.com.my/property-for-sale?freetext=Kuala+Lumpur",
        "https://www.propertyguru.com.my/property-for-rent?freetext=Selangor",
        "https://www.propertyguru.com.my/property-for-sale/2?freetext=Kuala+Lumpur",
        "https://www.propertyguru.com.my/condo-for-sale",
        "https://www.propertyguru.com.my/condo-for-sale/in-kuala-lumpur-58jok",
        "https://www.propertyguru.com.my/new-property-launch?region_code=58jok",
        "https://www.propertyguru.com.my/property-listing/kuala-lumpur-kuala-lumpur-kuala-lumpur-for-sale-by-kane-chai-36124001",
        "https://www.propertyguru.com.my/agent/kane-chai-12690529",
    ],
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "MY",
    },
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/propertyguru-my-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 '{
  "urls": [
    "https://www.propertyguru.com.my/property-for-sale?freetext=Kuala+Lumpur",
    "https://www.propertyguru.com.my/property-for-rent?freetext=Selangor",
    "https://www.propertyguru.com.my/property-for-sale/2?freetext=Kuala+Lumpur",
    "https://www.propertyguru.com.my/condo-for-sale",
    "https://www.propertyguru.com.my/condo-for-sale/in-kuala-lumpur-58jok",
    "https://www.propertyguru.com.my/new-property-launch?region_code=58jok",
    "https://www.propertyguru.com.my/property-listing/kuala-lumpur-kuala-lumpur-kuala-lumpur-for-sale-by-kane-chai-36124001",
    "https://www.propertyguru.com.my/agent/kane-chai-12690529"
  ],
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "MY"
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false
}' |
apify call abotapi/propertyguru-my-scraper --silent --output-dataset

```

## MCP server setup

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