# PropertyGuru MY $1💰 Search By URLs and Keywords (`abotapi/propertyguru-my-scraper`) Actor

From $1/1K. Fast, reliable scraper for propertyguru.com.my. Extract sale and rent listings with 30+ structured fields including price (MYR), built-up area, tenure, nearby transit, agent details, and GPS coordinates.

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

## PropertyGuru Malaysia Scraper

Fast, reliable scraper for [propertyguru.com.my](https://www.propertyguru.com.my) - Malaysia's largest property portal. Extract sale and rent listings with 30+ structured fields including price (MYR), built-up area, tenure, nearby transit, agent details, and GPS coordinates.

### Features

- **High throughput** - 20 listings per page, ~60 listings/min with pagination
- **Sale & Rent** listings with full filter support
- **Property types**: Condo / Apartment, Landed (terrace houses), Commercial (shop / office / factory), Residential Land
- **30+ data fields** per listing including price, PSF, features, transit distance, agent info
- **Coordinate enrichment** via detail-page extraction (lat/lng)
- **Robust** - handles site protection automatically
- **State-aware** - dropdown for all 13 states + 3 federal territories, plus free-text fallback for towns/neighbourhoods

### Quick Start

#### All properties for sale in Kuala Lumpur

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

#### Condos for rent in Mont Kiara

```json
{
  "mode": "search",
  "listing_type": "rent",
  "property_type": "condo",
  "search": "Mont Kiara"
}
```

#### Landed houses in Selangor under MYR 1,500,000

```json
{
  "mode": "search",
  "listing_type": "sale",
  "property_type": "landed",
  "state": "selangor",
  "bedrooms": 4,
  "max_price": 1500000
}
```

#### Commercial properties in Penang

```json
{
  "mode": "search",
  "listing_type": "sale",
  "property_type": "commercial",
  "state": "penang"
}
```

#### Custom URL mode

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

### Input Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `mode` | string | `search` | `search` (build URL from filters) or `url` (use provided URLs) |
| `urls` | array | - | URLs to scrape (url mode only) |
| `listing_type` | string | `sale` | `sale` or `rent` |
| `property_type` | string | - | `condo`, `landed`, `commercial`, or `land` |
| `state` | string | - | Malaysian state slug (see table below). Pick this OR `search`, not both. |
| `search` | string | - | Free-text location (e.g. "Mont Kiara", "Bangsar", "Cyberjaya") |
| `min_price` | integer | - | Minimum price in MYR |
| `max_price` | integer | - | Maximum price in MYR |
| `bedrooms` | integer | - | Number of bedrooms (0=Studio) |
| `sort` | string | `date` | Sort by `date`, `price`, or `psf` |
| `sort_order` | string | `desc` | `asc` or `desc` |
| `max_properties` | integer | `10` | Max properties to scrape (0=unlimited) |
| `max_pages` | integer | `20` | Max search result pages (20 listings/page) |
| `enable_detail_pages` | boolean | `true` | Fetch detail pages for richer fields (coords, description, facilities, schools) |
| `proxy` | object | Apify Residential MY | Proxy configuration |
| `resumeFromRunId` | string | - | Continue one interrupted run/dataset without re-collecting or re-charging listings already in it |
| `incrementalMode` | boolean | `false` | Track this same search/URL setup run-to-run and only return NEW/UPDATED/REAPPEARED listings |
| `stateKey` | string | - | Name an incremental-mode monitoring campaign (auto-derived from the search/filter/URL setup if left empty) |
| `emitUnchanged` | boolean | `false` | Incremental mode: also return (and bill) listings unchanged since the last run |
| `emitExpired` | boolean | `false` | Incremental mode: also return (and bill) listings no longer found, once a run fully scans the tracked search |

> **Note**: `state` and `search` are mutually exclusive. If both are set, `search` wins. `property_type` filters via URL path; all other filters go in the query string and can be freely combined.

### Resume & recurring updates

Two distinct workflows, both under the **Resume & recurring updates** input section:

- **Resume** (`resumeFromRunId`) continues **one** specific interrupted crawl. Paste the run or dataset ID from the earlier run; this run skips every listing id already collected there instead of re-scraping and re-billing it.
- **Incremental mode** (`incrementalMode`) is for scheduling the **same** search/URL setup repeatedly (e.g. daily). The actor remembers what it saw last time (keyed by mode + search/state/listing\_type/property\_type/bedrooms/min\_price/max\_price/sort/sort\_order/URLs, or your own `stateKey`) and classifies every listing as `NEW`, `UPDATED`, `UNCHANGED`, `REAPPEARED`, or `EXPIRED`. Only `NEW`/`UPDATED`/`REAPPEARED` are returned by default — turn on `emitUnchanged` or `emitExpired` if you also want those rows (both cost extra, billed dataset items). `EXPIRED` rows are only produced when a run reaches the natural end of the tracked search (not truncated by `max_properties`/`max_pages`, a blocked page, or `resumeFromRunId`) — otherwise the previous state for those listings is kept as-is and no `EXPIRED` rows are emitted that run.
- Incremental-mode output rows add `changeType`, `changedFields`, `firstSeenAt`, and `lastSeenAt`. These fields only appear when `incrementalMode` is on; a normal run's output shape is unchanged.

### Malaysian States & Federal Territories

| Slug | Name | Slug | Name |
|------|------|------|------|
| `kuala-lumpur` | Kuala Lumpur (FT) | `terengganu` | Terengganu |
| `selangor` | Selangor | `kelantan` | Kelantan |
| `penang` | Penang (Pulau Pinang) | `perlis` | Perlis |
| `johor` | Johor | `melaka` | Melaka (Malacca) |
| `perak` | Perak | `sabah` | Sabah |
| `kedah` | Kedah | `sarawak` | Sarawak |
| `pahang` | Pahang | `putrajaya` | Putrajaya (FT) |
| `negeri-sembilan` | Negeri Sembilan | `labuan` | Labuan (FT) |

### 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 Fields

Each listing record contains 30+ fields:

| Category | Fields |
|----------|--------|
| **Identity** | `id`, `title`, `address`, `url`, `status` |
| **Price** | `price`, `price_formatted`, `currency` (MYR), `price_psf` |
| **Features** | `bedrooms`, `bathrooms`, `floor_area`, `land_area`, `tenure` |
| **Property** | `property_type`, `property_type_group`, `badges` |
| **Location** | `state`, `region`, `city`, `nearby_mrt` |
| **Coordinates** | `latitude`, `longitude`, `location_source` |
| **Agent** | `agent_name`, `agent_id`, `agent_license`, `agency_name` |
| **Developer** | `developer`, `is_developer_listing` |
| **Media** | `images[]`, `image_count` |
| **Dates** | `posted_date`, `posted_unix`, `recency` |

Detail-page enrichment (when `enable_detail_pages=true`) adds: `description`, `headline`, `latitude`/`longitude`, `project_name`, `total_units`, `tenure_detail`, `furnishing`, `floor_plans[]`, `virtual_tour_url`, `nearby_schools[]`, `agent_mobile`, `reference_number`, and more.

# 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. Provide propertyguru.com.my search or listing URLs. Tip: open propertyguru.com.my in your browser, apply filters, then paste the resulting URL here. 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`):

Apify Residential in Malaysia is required - PropertyGuru is geo-restricted to MY IPs. IP rotation is handled automatically. Free-tier accounts automatically use a more conservative path that disables detail-page enrichment for reliability; paid plans get the full feature set.

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

Number of parallel detail-page fetches. Higher values are faster but may trigger rate limiting; 1-3 is recommended.

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

How often the scraper refreshes its connection state. Lower values are more conservative but slightly slower; higher values run faster at slight risk of a mid-run block. The default of 10 minutes comfortably covers typical runs.

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

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 - skipping the detail-page fetch entirely. Dramatically reduces cost on condo-heavy runs at the expense of per-listing fields (description, floor plans, unit number). Disable to fetch every listing's detail page.

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

Custom dataset name (leave empty for default)

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

Clear the dataset before scraping

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

## `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"
  ],
  "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"
    ],
    "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"],
    "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"
  ],
  "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/1NSGZ51j7WIEo9byO/openapi.json
