# iProperty MY Scraper - Properties, Prices, Agents & Photos (`abotapi/iproperty-com-my-scraper`) Actor

Scrape property listings from iProperty.com.my for sale or rent: price, PSF, bedrooms, bathrooms, floor area, tenure, property type, full address, agent (name, license, agency, contact numbers) and photo gallery. Search and URL modes, filters, 6 sort orders, and optional detail enrichment.

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

## Pricing

from $1.00 / 1,000 listing 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

## iProperty.com.my Scraper

Pull structured property listings from iProperty.com.my at scale. Search by channel (for sale or for rent), location and filters, or paste any iProperty.com.my search or listing URL, and get a clean record per property with price, price type, price-per-square-foot, bedrooms, bathrooms, floor area, tenure, property type, the full address with region / district / area, the photo gallery, the listing agent (name, license, verified status, profile) and agency, and the aggregate agent rating. Two modes (search and URL), 6 sort orders, and location, property-type, price and bedroom filters. An optional detail pass adds the full property description, map coordinates, and the listing agent's contact numbers.

### Why This Scraper?

- \~50 fields per listing, including price type (e.g. Negotiable), price-per-square-foot, tenure, the full photo gallery and the agent / agency profile.
- Two ways in: build a search from channel + location + filters, or paste search and listing URLs directly. Filter fields are ignored in URL mode so pasted links are used as-is.
- 6 sort orders (price, date, built-up size) and location, property-type, price and bedroom filters.
- Optional detail enrichment adds the full property description, the map coordinates (latitude / longitude), and the listing agent's contact numbers.
- One simple cap: set Max items and the run stops there once enough results are collected.
- Export results straight into Notion, Linear, Airtable or Apify via MCP connectors, without touching the dataset.

### A note on agent reviews

iProperty.com.my does **not** publish per-comment agent reviews on its public pages. There is no list of individual review texts to scrape. The only review signal the public site exposes is an **aggregate agent rating**, returned here as `agentRating` with `reviewCount` and `ratingAverage` (these are commonly `0`). This scraper returns that aggregate and does not invent per-comment reviews.

The listing agent's direct phone number is gated behind an on-page verification step and is not present in the page data, so `agent.phone` is intentionally left out rather than guessed.

### Data You Get

> Sample shape - values are illustrative placeholders, not from a live listing.

| Field | Example |
| --- | --- |
| listingId | 501630772 |
| title | Taman Wawasan, Puchong |
| propertyType | 2-storey Terraced House |
| priceValue | 750000 |
| pricePretty | RM 750,000 |
| priceType | Negotiable |
| psf | RM 535.71 psf |
| bedrooms | 4 |
| bathrooms | 3 |
| floorArea | 1800 |
| tenure | Freehold |
| fullAddress | Jalan Wawasan, Taman Wawasan, Puchong, Selangor |
| agent.name | Example Agent |
| agent.license | REN 00000 |
| agency.name | Example Realty Sdn. Bhd. |
| agentRating | { reviewCount: 0, ratingAverage: 0 } |
| imageCount | 6 |
| thumbnail | https://example-cdn/listing/501630772/thumb.jpg |
| url | https://www.iproperty.com.my/property/puchong/taman-wawasan/sale-501630772/ |

Detail fields (when Fetch full details is on): `description`, `latitude`, `longitude`, `agentPhone`, `agentContacts`.

### How to Use

Search for sale in a location:

```json
{
  "mode": "search",
  "channel": "sale",
  "location": "Kuala Lumpur",
  "sortBy": "price_asc",
  "maxItems": 50
}
```

Filter by price and bedrooms:

```json
{
  "mode": "search",
  "channel": "sale",
  "priceMin": 300000,
  "priceMax": 800000,
  "bedrooms": 3,
  "maxItems": 100
}
```

Paste iProperty.com.my URLs (filters ignored):

```json
{
  "mode": "url",
  "urls": [
    "https://www.iproperty.com.my/property-for-rent"
  ],
  "fetchDetails": true,
  "maxItems": 80
}
```

### Input Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| mode | string | `search` (build a search from channel + location + filters) or `url` (use pasted URLs). |
| channel | string | `sale` or `rent`. |
| location | string | Optional. A state, city, district or neighbourhood name: `Kuala Lumpur`, `Selangor`, `Petaling Jaya`, `Mont Kiara`. Hyphens are accepted too (`petaling-jaya`). An iProperty.com.my location slug copied from your address bar (`in-kuala-lumpur-58jok`) is also accepted verbatim. Empty = whole country. |
| propertyType | string | Optional property-type filter: `bungalow`, `condo` (includes apartments/service residences), `semi_detached`, `terrace`, `land`. Empty = all types. |
| priceMin | integer | Minimum price (MYR). |
| priceMax | integer | Maximum price (MYR). |
| bedrooms | integer | Minimum number of bedrooms. |
| sortBy | string | One of 6 sort orders (price, date, built-up size). The site has no bedroom-count sort. |
| urls | array | iProperty.com.my search or listing URLs (URL mode). |
| fetchDetails | boolean | Visit each listing page for the full description, map coordinates and agent contact numbers. |
| maxItems | integer | The single cap on the run. 0 = unlimited. Default 20. |
| maxPages | integer | Optional safety bound on pages per search. Empty = unlimited; the run stops at Max items. |
| proxy | object | Proxy configuration. Apify Residential recommended. |
| maxResidentialMB | integer | Residential traffic budget in MB. Caps how much residential traffic a run may use; the run keeps working as long as the site still serves results. 0 = unlimited. |
| resumeFromRunId | string | Continue one specific interrupted run — paste a previous run or dataset ID and this run skips listings already collected there. |
| incrementalMode | boolean | Turn this on for daily/recurring monitoring of the same search. The first run returns everything as `NEW`; later runs return only `NEW`/`UPDATED`/`REAPPEARED` by default. |
| stateKey | string | Optional. Name a monitoring campaign to keep its state stable, or to deliberately share state across differently-configured runs. Leave empty for an automatic key derived from the search/URL and detail settings. |
| emitUnchanged | boolean | Off by default. Also return `UNCHANGED` listings (incremental mode) — returns and bills extra rows you already have. |
| emitExpired | boolean | Off by default. Also return `EXPIRED` listings no longer found since the last run (incremental mode) — only produced after a complete, non-resumed, non-capped scan. Returns and bills extra synthetic rows. |
| mcpConnectors | array | Optional MCP connectors to pipe results into (Notion, Linear, Airtable, Apify). |
| notionParentPageUrl | string | Notion parent page URL/id for the Notion connector. |
| maxNotifyListings | integer | Cap on items written to each connector per run. |

### Resume & recurring updates

Two different features, both optional:

- **Resume** (`resumeFromRunId`) continues one specific interrupted crawl: paste a previous run or dataset ID and this run skips every listing already collected there. No state is kept between unrelated runs.
- **Incremental mode** (`incrementalMode`) is for scheduling this same search daily/weekly and getting only what changed. The actor remembers its own baseline for the search (keyed by `stateKey`, or an automatic key derived from the search/URL and detail settings) and classifies every listing as `NEW`, `UPDATED`, `UNCHANGED`, `REAPPEARED`, or `EXPIRED`. By default only `NEW`/`UPDATED`/`REAPPEARED` listings return; turn on `emitUnchanged`/`emitExpired` to also get those (both return — and bill — extra rows). `EXPIRED` listings are only produced after a run that scanned its full search with no cap hit and no Resume.

Output then carries `changeType`, `changedFields`, `firstSeenAt`, and `lastSeenAt`. Change detection ignores `scrapedAt`/`scraped_at` (per-run timestamps) and `searchMetadata` (search-relative), and it ignores fields that only exist once the optional detail fetch has run — `description`, `latitude`, `longitude`, `agentContacts`, `agentPhone`, and the full `images`/`imageCount` gallery — since none of those can be compared across runs where detail fetching was toggled on or off. `recency` (e.g. `"Listed on Aug 05, 2026 (55s ago)"`) is normalised, not ignored: the trailing `(... ago)` relative-age clock is stripped before comparison so it never re-triggers `UPDATED` on its own, but a genuine change to the listed date still does. Price, bedrooms, bathrooms, floor area, address, and every other core field **do** trigger `UPDATED` when they change.

### Send results into your apps (MCP connectors)

Optionally pipe each run's results into the tools you already use through Model Context Protocol (MCP) connectors. Authorize a connector under Apify, Settings, API & Integrations, then select it in `mcpConnectors`. Notion receives a rich page per listing (set `notionParentPageUrl`); Linear, Airtable and Apify get a best-effort write or digest. The MCP export never changes the dataset and never fails the run; `maxNotifyListings` caps how many items go to each connector.

### Output Example

> Sample shape - values are illustrative placeholders, not from a live listing.

```json
{
  "listingId": 501630772,
  "url": "https://www.iproperty.com.my/property/puchong/taman-wawasan/sale-501630772/",
  "title": "Taman Wawasan, Puchong",
  "propertyType": "2-storey Terraced House",
  "typeText": "For Sale",
  "price": { "value": 750000, "pretty": "RM 750,000", "currency": "MYR", "type": "Negotiable" },
  "priceValue": 750000,
  "pricePerArea": "RM 416.67 psf",
  "psf": "RM 535.71 psf",
  "bedrooms": 4,
  "bathrooms": 3,
  "floorArea": 1800,
  "tenure": "Freehold",
  "location": { "area": "Taman Wawasan", "district": "Puchong", "region": "Selangor" },
  "fullAddress": "Jalan Wawasan, Taman Wawasan, Puchong, Selangor",
  "agent": { "id": 0, "name": "Example Agent", "license": "REN 00000", "isVerified": true, "profileUrl": "https://www.iproperty.com.my/property-agent/example-0/" },
  "agency": { "id": 0, "name": "Example Realty Sdn. Bhd." },
  "agentRating": { "reviewCount": 0, "ratingAverage": 0 },
  "images": ["https://example-cdn/listing/501630772/photo-1.jpg"],
  "imageCount": 6,
  "thumbnail": "https://example-cdn/listing/501630772/thumb.jpg",
  "description": "Full property description (when Fetch full details is on).",
  "latitude": 3.04,
  "longitude": 101.62,
  "source": "iproperty.com.my",
  "scrapedAt": "2024-01-01T00:00:00+00:00"
}
```

### Plan Requirement

Runs on any Apify plan. This site serves best on residential connections, so set the proxy to Apify Residential for reliable results (Residential access is included on the Starter plan and higher). Datacenter and free-tier connections are commonly rejected and will return zero items; if you run on datacenter, a free tier or a non-residential connection the run logs a warning before spending compute.

#### Reliability

- Built for resilient, unattended runs: transient errors are recovered automatically so a run keeps working without manual intervention.
- A page that does not exist (HTTP 404) is reported immediately as `PAGE_NOT_FOUND` instead of being retried. It means the Location value or a pasted URL points at a page iProperty.com.my does not serve, so the run tells you to check your input rather than spending time and money re-requesting a page that can never load. Changing the proxy settings will not help in that case.
- Residential traffic budget: set `maxResidentialMB` to cap residential spend. The run keeps returning results as long as the site still serves them, and will not silently drop to zero. 0 = unlimited.

# Actor input Schema

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

How to start the scrape. 'search' builds a search from the channel, location and filters below. 'url' walks the iProperty.com.my search or listing URLs you paste (search filters below are ignored in URL mode).

## `channel` (type: `string`):

Search properties for sale or for rent.

## `location` (type: `string`):

Optional location to narrow the search: a state, city, district or neighbourhood name (e.g. 'Kuala Lumpur', 'Selangor', 'Petaling Jaya', 'Mont Kiara'). Hyphens work too ('petaling-jaya'). You can also paste an iProperty.com.my location slug straight from your browser's address bar (e.g. 'in-kuala-lumpur-58jok'). Leave empty to search the whole country.

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

Optional property type filter. Leave empty for all types.

## `priceMin` (type: `integer`):

Only return listings priced at or above this amount.

## `priceMax` (type: `integer`):

Only return listings priced at or below this amount.

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

Only return listings with at least this many bedrooms.

## `sortBy` (type: `string`):

Order the results are returned in.

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

Paste iProperty.com.my search URLs (https://www.iproperty.com.my/property-for-sale...) or listing URLs (https://www.iproperty.com.my/property/.../sale-<id>/). Multiple URLs supported; search filters are ignored.

## `fetchDetails` (type: `boolean`):

Visit each listing page to add the full property description and map coordinates (latitude / longitude). Slower and adds a small per-item charge; leave off for fast basic listings.

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

The single cap on this run: stop after collecting this many listings (across all searches/URLs). Set 0 for unlimited.

## `maxPages` (type: `integer`):

Optional safety bound on how many result pages to include per search. Defaults to unlimited (leave empty or set 0) and does NOT cap the run; it defers to Max items, which is the single cap. The run stops at Max items, not here.

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

Paste a previous run ID or dataset ID to continue a large crawl without returning or charging for listings already collected there. Use this after an interrupted run. For recurring 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/URL and detail-mode 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/URL and detail 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 items capped it or when Resume was used. This returns — and bills — extra synthetic rows, so leave it off unless you need expiry tracking.

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

Use an Apify Residential proxy for reliable results.

## `maxResidentialMB` (type: `integer`):

When using a Residential proxy group, cap residential traffic at this many MB. The run keeps returning results as long as the site still serves them, and will not silently drop to zero. 0 = unlimited.

## `mcpConnectors` (type: `array`):

Optionally send results into the apps you already use, via Model Context Protocol (MCP) connectors. Authorize one under Apify, Settings, API & Integrations, then select it here. Notion gets a rich page-per-item export; other connectors get a best-effort write/digest. Leave empty to skip; never changes the dataset output. 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",
  "channel": "sale",
  "location": "Kuala Lumpur",
  "propertyType": "",
  "sortBy": "relevance",
  "urls": [
    "https://www.iproperty.com.my/property-for-sale"
  ],
  "fetchDetails": false,
  "maxItems": 20,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "MY"
  },
  "maxResidentialMB": 0,
  "maxNotifyListings": 50
}
```

# Actor output Schema

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

No description

# 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",
    "location": "Kuala Lumpur",
    "urls": [
        "https://www.iproperty.com.my/property-for-sale"
    ],
    "maxItems": 20,
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false,
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "MY"
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/iproperty-com-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 = {
    "mode": "search",
    "location": "Kuala Lumpur",
    "urls": ["https://www.iproperty.com.my/property-for-sale"],
    "maxItems": 20,
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "MY",
    },
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/iproperty-com-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 '{
  "mode": "search",
  "location": "Kuala Lumpur",
  "urls": [
    "https://www.iproperty.com.my/property-for-sale"
  ],
  "maxItems": 20,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "MY"
  }
}' |
apify call abotapi/iproperty-com-my-scraper --silent --output-dataset

```

## MCP server setup

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