# 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
- **Features:** Limited permissions
- **Stats:** 5 total users, 0 monthly users, 100.0% runs succeeded, 1 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?

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

## iProperty.com.my Scraper: Property Listings, Prices, Agents & Photos

iProperty.com.my Scraper turns iProperty.com.my search results into structured property data. Search for sale or rent listings by channel, location, property type, price range and minimum bedrooms, or paste iProperty.com.my search and listing URLs directly, and get a record per property with price, price type, price-per-square-foot, bedrooms, bathrooms, floor area, tenure, the full address with region / district / area, the photo gallery, and the listing agent's name, license, verified status and agency. Two modes (search and URL), 6 sort orders, and location, property-type, price and bedroom filters. An optional detail pass adds the full description, map coordinates, and the agent's phone / WhatsApp contact numbers. Export results to JSON, CSV or Excel, or pull them straight into your app through the API.

### Why This Scraper?

- **~50 fields.** ~50 fields on a search-mode listing: price type (e.g. Negotiable), price-per-square-foot, tenure, the full photo gallery, the region / district / area breakdown, and the agent / agency profile.
- **Two ways in.** Two ways in: build a search from channel + location + filters, or paste iProperty.com.my search and listing URLs directly. Search filters are ignored in URL mode so pasted links are used as-is.
- **Sorts and filters.** 6 sort orders (price, date, built-up size) and location, property-type, price and bedroom filters; the location box accepts plain text ("Petaling Jaya") or the site's own location slug pasted from your address bar.
- **Detail enrichment.** Optional detail enrichment adds the full property description, map coordinates (latitude / longitude), and the listing agent's phone and WhatsApp numbers, billed only on the listings it actually runs for.
- **Built for schedules.** Built for schedules: Incremental mode returns only new and changed listings on recurring runs, and unchanged rows are never billed unless you ask for them.
- **Export anywhere.** Export results straight into Notion, Linear, Airtable or Apify via MCP connectors, without touching the dataset.

### Use Cases

- **Property portals and lead generation:** build a searchable inventory of for-sale or for-rent listings with price, address and agent contact for outreach.
- **Market research and pricing analysis:** track price-per-square-foot and asking prices across regions and property types over time.
- **Listing monitoring:** watch a specific location or price band on a schedule and get only the new and changed listings, without re-collecting everything.
- **Investment screening:** filter by price range, bedrooms and property type to shortlist properties matching an investment thesis.
- **App and CRM feeds:** pipe structured listings into your own site, CRM or spreadsheet through the API or MCP connectors.

### Data You Get

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

| Field | Example |
| --- | --- |
| `listingId` | `900000001` |
| `title` | `"Sample Listing, Sample Area"` |
| `propertyType` | `"Condominium"` |
| `typeText` | `"For Sale"` |
| `price` | `{ "value": 650000, "pretty": "RM 650,000", "currency": "MYR", "type": "Negotiable" }` |
| `priceType` | `"Negotiable"` |
| `psf` | `"RM 480.00 psf"` |
| `bedrooms` / `bathrooms` | `3` / `2` |
| `floorArea` | `1100` |
| `tenure` | `"Freehold"` |
| `location` | `{ "area": "Sample Area", "district": "Sample District", "region": "Selangor" }` |
| `fullAddress` | `"Jalan Contoh, Sample Area, Sample District, Selangor"` |
| `agent.name` / `agent.license` | `"Sample Agent"` / `"REN 00001"` |
| `agent.isVerified` | `true` |
| `agency.name` | `"Sample Realty Sdn. Bhd."` |
| `agentRating` | `{ "reviewCount": 0, "ratingAverage": 0 }` |
| `imageCount` | `1` (matches the length of `images`) |
| `thumbnail` | `"https://cdn.example.com/listing/900000001/thumb.jpg"` |
| `recency` | `"Listed on Jan 05, 2026 (2h ago)"` |
| `url` | `"https://www.iproperty.com.my/property/sample-area/sample-project/sale-900000001/"` |
| `source` | `"iproperty.com.my"` |

Every search-card record also carries flat `furnishing`, `listingType` (`sale` or `rent`), `isCommercial`, `currency`, `area`, `district`, `region`, `agentName`, `agencyName` and `recencyText` alongside the nested objects. Tick **Commercial properties** (`isCommercial`) to search the commercial sale or rent routes instead of residential listings.

Search-mode listings (and URL-mode listings pasted as a search URL) also carry `propertyTypeGroup`, `statusCode`, `externalId`, `searchUrl`, `shortAddress`, `pricePerArea`, `areaText`, `badges`, `postedOn`, `developer`, `isOfficialListing`, `isDeveloperListing`, `newProject` and `projectId`. iProperty.com.my does not publish per-comment agent reviews on its public pages, so `reviews` is always returned as `null`; `agentRating` (above, commonly `0`/`0`) is the only review signal the site exposes. With **Fetch full details** on, a record adds `description`, `latitude`, `longitude`, `agentContacts` (a list of the agent's phone / WhatsApp numbers) and `agentPhone` whenever the listing page provides them (a failed detail fetch is skipped silently and ships without enrichment), and the `images`/`imageCount` gallery is replaced by the fuller detail-page gallery. **Pasting a single listing URL (not a search URL) is a narrower shape**: it never goes through a search results card, so `price`, `bedrooms`, `bathrooms`, `tenure`, `fullAddress`, `location`, `agent` and `agency` are present as keys but always null or empty (`images: []`, `imageCount: 0`), alongside the real `listingId`, `id`, `url`, `source`, `listingUrl` and `scrapedAt`, plus the detail-only fields above when Fetch full details is on. Use Search mode, or paste a search URL, to get the full record.

### How to Use

1. Pick a **mode**: `search` (build from channel + location + filters) or `url` (paste iProperty.com.my search or listing URLs).
2. In search mode, set the channel, an optional location and any filters. In URL mode, paste your links; search filters are ignored.
3. Turn on **Fetch full details** if you need the description, coordinates or agent contact numbers, and set **Max items** to control run size and cost.
4. Click **Start**, then download the dataset as JSON, CSV or Excel, or read it through the API.

**Search for sale in a location:**

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

**Filter rentals by price and bedrooms:**

```json
{
  "mode": "search",
  "channel": "rent",
  "priceMin": 1500,
  "priceMax": 3500,
  "bedrooms": 3,
  "propertyType": "condo",
  "maxItems": 100
}
```

**Paste a search URL, with full details on:**

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

**Daily monitoring with expiry tracking:**

```json
{
  "mode": "search",
  "channel": "sale",
  "location": "Mont Kiara",
  "incrementalMode": true,
  "emitExpired": true,
  "maxItems": 0
}
```

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/iproperty-com-my-scraper").call(
    run_input={"mode": "search", "channel": "sale", "location": "Petaling Jaya", "maxItems": 50}
)
for listing in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(listing["title"], listing["pricePretty"])
```

JavaScript:

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

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('abotapi/iproperty-com-my-scraper').call({
  mode: 'search', channel: 'sale', location: 'Petaling Jaya', maxItems: 50,
});
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

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 or 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 `NEW`, `UPDATED`, `UNCHANGED`, `REAPPEARED` or `EXPIRED`. By default only `NEW`/`UPDATED`/`REAPPEARED` listings ship; turn on `emitUnchanged`/`emitExpired` to also get those (both return, and bill, extra rows). `EXPIRED` rows are only produced once a run has scanned its full tracked search with no cap hit, no Resume and no pasted URL that starts past page 1. A listing is `UPDATED` when any output field changes except the ignored ones (see `ignoreFieldsForChanges`); `changedFields` names what moved. `REAPPEARED` never happens "by default": a listing can only be marked absent (the one precondition for `REAPPEARED`) by an earlier run that had `emitExpired` on and completed that kind of full, uncapped, non-resumed scan. If `emitExpired` has never been turned on for a state key, `REAPPEARED` cannot occur for it yet.

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 detail-page `images`/`imageCount` gallery) whenever the two sides being compared were not fetched at the same depth, so a flaky or toggled detail fetch never manufactures a false `UPDATED`. `recency` and its flat copy `recencyText` (e.g. `"Listed on Aug 05, 2026 (55s ago)"`) are 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.

#### URL mode: supported URL shapes

Pasted search filters (sort, price, bedroom, property type, keyword) and page numbers (`?page=N` or a trailing `/N`) are applied as written and win over the search fields. A pasted page number starts the walk at that page, so the run is a partial scan and never marks listings `EXPIRED`. **If you switch to URL mode and leave the URLs field untouched, the run scrapes one example of each of the 13 supported shapes below (rows 1 to 13); raise Max items to reach all of them, because the default cap of 20 is spent on the first search URLs.** **Bedroom and price filters on new-project cards:** the site matches a new-launch project to a bedroom or price filter when any of its unit types qualifies, and its search card then shows the project's smallest unit. So a search with the 3-bedroom filter can return a project card whose own `bedrooms` is 2 (the project also lists a 3-bedroom unit type), and project cards carry `priceValue` 0 with the real asking text (a starting price, a range or "Price on ask") in `pricePretty` and `priceType`. These cards come from the site, not from a dropped or mis-encoded filter; resale cards always match the filter exactly. Each `searchMetadata` carries the totals of its own search URL. Agent profile pages are skipped with a loud warning in the log; if every pasted URL is an agent page, the run fails with a clear message instead of returning nothing silently.

| # | Shape | Example path |
|---|-------|--------------|
| 1 | Country-wide sale search | `/property-for-sale` |
| 2 | Country-wide rent search | `/property-for-rent` |
| 3 | Location search | `/property-for-sale/in-kuala-lumpur-58jok` |
| 4 | Paginated search | `/property-for-sale?page=2` |
| 5 | Paginated location search | `/property-for-sale/in-kuala-lumpur-58jok?page=2` |
| 6 | Filtered and sorted search | `/property-for-sale?sort=price&order=desc&propertyTypeGroup=N&minPrice=500000&maxPrice=900000&bedroom=3` |
| 7 | Keyword location search | `/property-for-sale?keyword=Mont%20Kiara` |
| 8 | Property-type category | `/terrace-for-sale` |
| 9 | Property-type category (long slug) | `/apartment-condo-service-residence-for-sale` |
| 10 | Property-type category (commercial) | `/commercial-property-for-sale` |
| 11 | Listing detail page | `/property/bangsar/khaya-residences/sale-501740698/` |
| 12 | Listing detail page (id only) | `/property/sale-501740698/` |
| 13 | New-project listing detail page | `/new-property/property/johor-bahru/residensi-maxim-pelangi-the-address-johor-bahru/sale-501752866/` |
| 14 | Agent profile page (not supported, skipped with a warning; not part of the default list) | `/property-agent/belinda-choo-3310039/` |

### 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. This is an extra delivery step after the scrape: the Apify dataset is never changed, and a failed export never fails the run.

1. Authorize a connector once under Apify → Settings → Integrations (Notion, Linear, Airtable, or Apify).
2. Select it in the `mcpConnectors` input field.
3. For **Notion**, also set `notionParentPageUrl`: Notion gets one rich page per listing (up to `maxNotifyListings`, default 50). Linear, Airtable and Apify instead get a single combined note covering up to 25 of the collected listings, sent to whichever writable tool that connector exposes.

### Input Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `mode` | string | `search` | `search` (build from channel + location + filters) or `url` (use pasted URLs; filters below are ignored). |
| `channel` | string | `sale` | `sale` or `rent`. Search mode only. |
| `isCommercial` | boolean | `false` | Search mode only. Search the commercial sale or rent routes instead of residential listings. |
| `location` | string | no default (prefill: `Kuala Lumpur`) | Optional state, city, district or neighbourhood name (`Kuala Lumpur`, `Selangor`, `Petaling Jaya`, `Mont Kiara`). Hyphens work too (`petaling-jaya`). An iProperty.com.my location slug copied from the address bar (`in-kuala-lumpur-58jok`) is also accepted verbatim. Empty = whole country. |
| `propertyType` | string | \`\` (all types) | Optional property-type filter: `bungalow`, `condo` (includes apartments/service residences), `semi_detached`, `terrace`, `land`. |
| `priceMin` | integer | no default | Minimum price (MYR). |
| `priceMax` | integer | no default | Maximum price (MYR). |
| `bedrooms` | integer | no default | Minimum number of bedrooms. |
| `sortBy` | string | `relevance` | One of 6 sort orders (price, date, built-up size). The site has no bedroom-count sort. |
| `urls` | array | one example of each of the 13 supported URL shapes (see the table above) | iProperty.com.my search, category or listing URLs. URL mode only. |
| `fetchDetails` | boolean | `false` | Visit each listing page for the full description, map coordinates and agent contact numbers. Slower and adds a small per-item charge. |
| `maxItems` | integer | `20` | The single cap on the run: stop after collecting this many listings across all searches/URLs. `0` = unlimited. |
| `maxPages` | integer | no default (unlimited; defers to Max items) | Optional safety bound on result pages per search. Leave empty or `0` for unlimited; the run still stops at Max items. |
| `resumeFromRunId` | string | no default | Paste a previous run or dataset ID to continue a large crawl without returning or charging for listings already collected there. For recurring monitoring of the same search, use Incremental mode instead. |
| `ignoreFieldsForChanges` | array | `[]` | Incremental mode only. Extra field names (or nested paths such as `postedOn.unix`) that must not make a listing `UPDATED`; they stay in the output. Always ignored: `scrapedAt`, `searchMetadata`, the relative "ago" part of `recency`, `postedOn.text`, `thumbnail`, `images` (image links; `imageCount` is still compared). |
| `incrementalMode` | boolean | `false` | Turn on for daily/recurring monitoring of the same search. The first run returns everything as `NEW`; later runs normally return only `NEW`/`UPDATED`/`REAPPEARED`. |
| `stateKey` | string | no default | Optional. Name a monitoring campaign to keep its state stable, or to deliberately share state across differently-configured runs. Empty = an automatic key derived from the search/URL and detail settings. |
| `emitUnchanged` | boolean | `false` | Also return (and bill) `UNCHANGED` listings in incremental mode. |
| `emitExpired` | boolean | `false` | Also return (and bill) `EXPIRED` listings no longer found since the last run. Only produced after a complete, non-resumed, non-capped scan. |
| `proxy` | object | Residential (MY) | Proxy configuration. Apify Residential recommended. |
| `maxResidentialMB` | integer | `0` (unlimited) | Advisory residential traffic budget in MB, logged at startup. Does not currently stop or reroute traffic once a run is underway. |
| `mcpConnectors` | array | no default | Optional MCP connectors to pipe results into (Notion, Linear, Airtable, Apify). |
| `notionParentPageUrl` | string | no default | Notion parent page URL or id for the Notion connector. |
| `maxNotifyListings` | integer | `50` | Cap on items written to each connector per run. Does not affect the dataset. |

### Output Example

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

```json
{
  "listingId": 900000001,
  "url": "https://www.iproperty.com.my/property/sample-area/sample-project/sale-900000001/",
  "title": "Sample Listing, Sample Area",
  "propertyType": "Condominium",
  "typeText": "For Sale",
  "price": { "value": 650000, "pretty": "RM 650,000", "currency": "MYR", "type": "Negotiable" },
  "priceValue": 650000,
  "pricePretty": "RM 650,000",
  "priceType": "Negotiable",
  "pricePerArea": "RM 480.00 psf",
  "psf": "RM 480.00 psf",
  "bedrooms": 3,
  "bathrooms": 2,
  "floorArea": 1100,
  "tenure": "Freehold",
  "location": { "area": "Sample Area", "district": "Sample District", "region": "Selangor" },
  "fullAddress": "Jalan Contoh, Sample Area, Sample District, Selangor",
  "agent": {
    "id": 1,
    "name": "Sample Agent",
    "license": "REN 00001",
    "isVerified": true,
    "profileUrl": "https://www.iproperty.com.my/property-agent/sample-agent-1/"
  },
  "agency": { "id": 1, "name": "Sample Realty Sdn. Bhd." },
  "agentRating": { "reviewCount": 0, "ratingAverage": 0 },
  "images": ["https://cdn.example.com/listing/900000001/photo-1.jpg"],
  "imageCount": 1,
  "thumbnail": "https://cdn.example.com/listing/900000001/thumb.jpg",
  "recency": "Listed on Jan 05, 2026 (2h ago)",
  "description": "Full property description (Fetch full details on).",
  "latitude": 3.05,
  "longitude": 101.7,
  "agentContacts": [{ "type": "mobile", "value": "+60123456789" }],
  "agentPhone": "+60123456789",
  "reviews": null,
  "source": "iproperty.com.my",
  "scrapedAt": "2026-01-05T00:00:00+00:00"
}
```

### Plan Requirement

iProperty.com.my sits behind an edge bot challenge. In our measurements the challenge refused Apify datacenter and Apify Residential connections alike (any country), and the same page loaded only over a different, pooled connection type that the actor switches to on its own after repeated challenges. That backup connection is a proxy group your Apify plan has to be able to use, and free plans may not have it. So the honest answer is: there is no guarantee that a run works on every plan, and a residential proxy on its own is not enough for this site. Keep the default proxy setting; the actor handles the switch and tells you in the log when it happens.

#### Reliability

- Transient errors are retried on a fresh connection automatically.
- When the default connection is challenged several times in a row, the actor moves the rest of the run to the backup connection and logs: `Primary connection unavailable - trying the backup connection.` Once that connection serves data it logs once: `Served over the backup connection (the default connection was unavailable).`
- A run that loads no page at all ends FAILED, never as a silent empty success, with one of these status messages: `No results: residential proxies are unavailable on this run (Apify free plans do not include them), and the backup connection could not load the requested iProperty pages...` (proxy access refused, and the backup connection was reachable but the site still served no page), `The proxy connection was refused for this account, so no page could be read. Your Apify plan does not include proxy access; a paid plan is required...` (proxy access refused and the backup connection was refused too), or `Could not load any results page (the requests failed), so nothing was collected. This is a connection problem, not an empty search. Please try again later.`
- A page that does not exist (HTTP 404) is not retried, and a zero-item run is reported as `PAGE_NOT_FOUND`; it means the Location value or a pasted URL points at a page the site does not serve. If one search or pasted URL of several is blocked or 404s, the run stops there and keeps what it already collected; the run log notes what was skipped.
- Agent profile URLs are not supported: they are skipped with a warning, and a run where every URL is an agent page fails with a clear message.
- `maxResidentialMB` is advisory only, logged at startup. `0` = unlimited.

#### Views

The dataset has three views: **overview** (the main listing columns), **detail** (description, coordinates and agent contacts, filled when Fetch full details is on) and **changes** (`changeType`, `changedFields`, `firstSeenAt`, `lastSeenAt` for Incremental mode). In Incremental mode, `ignoreFieldsForChanges` lists extra fields (or nested paths such as `postedOn.unix`) that never make a listing `UPDATED`.

#### Billing

You are billed per event, with the current rates on the **Pricing** tab: the run start event (`apify-actor-start`, once per run), `listing-result` (once per listing pushed to the dataset) and `detail-enrichment` (once per listing whose detail page was fetched, only when Fetch full details is on). Listings that are suppressed, deduplicated or unchanged in Incremental mode, and failed detail fetches, are not charged. Use **Max items** to cap a run.

### FAQ

#### How much does it cost?

You pay per run start, per listing returned, and a smaller amount per listing whose detail page is fetched (only when Fetch full details is on). The **Pricing** tab shows the current rates for each of these. Use **Max items** to cap the cost of any run.

#### Is it legal to scrape iProperty.com.my?

This actor collects only publicly available property listing data. You are responsible for how you use it: follow iProperty.com.my's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution. Listing facts (price, size, address) are generally data, but photos and agent profile details 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 (once a prior complete scan has run with Emit expired on) reappeared listings; unchanged ones are not billed unless you turn on Emit unchanged.

#### Does this include individual agent review comments?

No. iProperty.com.my does not publish per-comment agent reviews on its public pages, so there is nothing to scrape there. The only review signal the site exposes is an aggregate `agentRating` (`reviewCount` / `ratingAverage`), which is commonly `0`.

#### What does a pasted single listing URL return?

Less than a search-mode record. A pasted listing detail URL never goes through a search results card, so price, bedrooms, bathrooms, tenure, address, agent and agency come back null or empty; you still get the listing id, URL and (with Fetch full details on) the description, coordinates and agent contact numbers. To get the full ~50-field record, use Search mode or paste a search URL instead of an individual listing URL.

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

A blocked connection does not fail the run; when it returns zero items, the run completes normally with a status message explaining why. When a block or 404 happens partway through a multi-search or multi-URL run after some items were already collected, that stop is visible only in the run log, not in a status message, and this site serves best on a residential proxy. An invalid `resumeFromRunId`, or turning on Resume for a search that already has saved Incremental mode state, does stop the run outright with a clear error, since continuing would double-count or silently discard that state. Separately, if a run covers several searches or pasted URLs and one of them returns HTTP 404 or is fully blocked, the run stops at that point and keeps whatever it already collected rather than continuing to the rest.

#### 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-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/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-sg-scraper"><b>PropertyGuru SG Scraper</b></a><br>Scrape PropertyGuru.com.sg sale and rental listings with 30+ structured fields, including...</td><td>🏠 <a href="https://apify.com/abotapi/zonaprop-scraper"><b>Zonaprop Argentina</b></a><br>Scrape Zonaprop.com.ar property listings across Argentina. Extract 140+ fields including...</td></tr>
<tr><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><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></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/iproperty-com-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`):

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.

## `isCommercial` (type: `boolean`):

Search the commercial property routes (offices, shops, retail, industrial) for the chosen channel instead of residential listings.

## `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, category or listing URLs (country-wide, location, ?page=N, filtered/sorted, keyword, property-type category, listing detail, new-project detail). Pasted filters and page numbers are applied; they win over the search fields. Agent profile pages are not supported and are skipped with a warning. Leave untouched in URL mode to scrape one example of each of the 13 supported shapes (raise Max items to reach all of them: the default cap of 20 is spent on the first search URLs).

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

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

## `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. A listing counts as UPDATED when any output field changes except the ignored ones (by default scrapedAt, searchMetadata, the relative ago-text in recency, recencyText and postedOn, thumbnail and image links; see Ignore fields below); changedFields names what moved. 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.

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

Incremental mode only. Field names (or nested paths such as postedOn.unix) that must NOT make a listing count as UPDATED. They stay in the output. Already ignored by default: scrapedAt, searchMetadata, the relative "(… ago)" part of recency and recencyText, postedOn.text, thumbnail and images (image CDN links). Everything else is compared, so a change to price, bedrooms, bathrooms, floor area, title, tenure, agent, description, coordinates or image count marks the listing UPDATED and lists it in changedFields.

## `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",
  "isCommercial": false,
  "location": "Kuala Lumpur",
  "propertyType": "",
  "sortBy": "relevance",
  "urls": [
    "https://www.iproperty.com.my/property-for-sale",
    "https://www.iproperty.com.my/property-for-rent",
    "https://www.iproperty.com.my/property-for-sale/in-kuala-lumpur-58jok",
    "https://www.iproperty.com.my/property-for-sale?page=2",
    "https://www.iproperty.com.my/property-for-sale/in-kuala-lumpur-58jok?page=2",
    "https://www.iproperty.com.my/property-for-sale?sort=price&order=desc&propertyTypeGroup=N&minPrice=500000&maxPrice=900000&bedroom=3",
    "https://www.iproperty.com.my/property-for-sale?keyword=Mont%20Kiara",
    "https://www.iproperty.com.my/terrace-for-sale",
    "https://www.iproperty.com.my/apartment-condo-service-residence-for-sale",
    "https://www.iproperty.com.my/commercial-property-for-sale",
    "https://www.iproperty.com.my/property/bangsar/khaya-residences/sale-501740698/",
    "https://www.iproperty.com.my/property/sale-501740698/",
    "https://www.iproperty.com.my/new-property/property/johor-bahru/residensi-maxim-pelangi-the-address-johor-bahru/sale-501752866/"
  ],
  "fetchDetails": false,
  "maxItems": 20,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "MY"
  },
  "maxResidentialMB": 0,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "ignoreFieldsForChanges": [],
  "maxNotifyListings": 50
}
```

# Actor output Schema

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

No description

## `detail` (type: `string`):

No description

## `changes` (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",
        "https://www.iproperty.com.my/property-for-rent",
        "https://www.iproperty.com.my/property-for-sale/in-kuala-lumpur-58jok",
        "https://www.iproperty.com.my/property-for-sale?page=2",
        "https://www.iproperty.com.my/property-for-sale/in-kuala-lumpur-58jok?page=2",
        "https://www.iproperty.com.my/property-for-sale?sort=price&order=desc&propertyTypeGroup=N&minPrice=500000&maxPrice=900000&bedroom=3",
        "https://www.iproperty.com.my/property-for-sale?keyword=Mont%20Kiara",
        "https://www.iproperty.com.my/terrace-for-sale",
        "https://www.iproperty.com.my/apartment-condo-service-residence-for-sale",
        "https://www.iproperty.com.my/commercial-property-for-sale",
        "https://www.iproperty.com.my/property/bangsar/khaya-residences/sale-501740698/",
        "https://www.iproperty.com.my/property/sale-501740698/",
        "https://www.iproperty.com.my/new-property/property/johor-bahru/residensi-maxim-pelangi-the-address-johor-bahru/sale-501752866/"
    ],
    "maxItems": 20,
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "MY"
    },
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false,
    "ignoreFieldsForChanges": []
};

// 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",
        "https://www.iproperty.com.my/property-for-rent",
        "https://www.iproperty.com.my/property-for-sale/in-kuala-lumpur-58jok",
        "https://www.iproperty.com.my/property-for-sale?page=2",
        "https://www.iproperty.com.my/property-for-sale/in-kuala-lumpur-58jok?page=2",
        "https://www.iproperty.com.my/property-for-sale?sort=price&order=desc&propertyTypeGroup=N&minPrice=500000&maxPrice=900000&bedroom=3",
        "https://www.iproperty.com.my/property-for-sale?keyword=Mont%20Kiara",
        "https://www.iproperty.com.my/terrace-for-sale",
        "https://www.iproperty.com.my/apartment-condo-service-residence-for-sale",
        "https://www.iproperty.com.my/commercial-property-for-sale",
        "https://www.iproperty.com.my/property/bangsar/khaya-residences/sale-501740698/",
        "https://www.iproperty.com.my/property/sale-501740698/",
        "https://www.iproperty.com.my/new-property/property/johor-bahru/residensi-maxim-pelangi-the-address-johor-bahru/sale-501752866/",
    ],
    "maxItems": 20,
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "MY",
    },
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
    "ignoreFieldsForChanges": [],
}

# 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",
    "https://www.iproperty.com.my/property-for-rent",
    "https://www.iproperty.com.my/property-for-sale/in-kuala-lumpur-58jok",
    "https://www.iproperty.com.my/property-for-sale?page=2",
    "https://www.iproperty.com.my/property-for-sale/in-kuala-lumpur-58jok?page=2",
    "https://www.iproperty.com.my/property-for-sale?sort=price&order=desc&propertyTypeGroup=N&minPrice=500000&maxPrice=900000&bedroom=3",
    "https://www.iproperty.com.my/property-for-sale?keyword=Mont%20Kiara",
    "https://www.iproperty.com.my/terrace-for-sale",
    "https://www.iproperty.com.my/apartment-condo-service-residence-for-sale",
    "https://www.iproperty.com.my/commercial-property-for-sale",
    "https://www.iproperty.com.my/property/bangsar/khaya-residences/sale-501740698/",
    "https://www.iproperty.com.my/property/sale-501740698/",
    "https://www.iproperty.com.my/new-property/property/johor-bahru/residensi-maxim-pelangi-the-address-johor-bahru/sale-501752866/"
  ],
  "maxItems": 20,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "MY"
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "ignoreFieldsForChanges": []
}' |
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/seevhawpRUMs3nnve/openapi.json
