# PropertyGuru SG $0.8💰 Search By URLs and Keywords (`abotapi/propertyguru-sg-scraper`) Actor

From $0.8/1K. Fast, reliable scraper for propertyguru.com.sg. Extract sale and rent listings with 30+ structured fields, including price, PSF, floor area, tenure, nearby MRT, agent details, and GPS

- **URL**: https://apify.com/abotapi/propertyguru-sg-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Categories:** Real estate, Automation, Developer tools
- **Stats:** 80 total users, 11 monthly users, 99.1% runs succeeded, 0 bookmarks
- **User rating**: 3.27 out of 5 stars

## Pricing

from $0.80 / 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 Singapore Scraper

Fast, reliable scraper for [propertyguru.com.sg](https://www.propertyguru.com.sg) - Singapore's largest property portal with 55,000+ active listings. Extract sale and rent listings with 30+ structured fields including price, PSF, floor area, tenure, nearby MRT, 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, HDB, Landed House
- **30+ data fields** per listing including price, PSF, features, MRT distance, agent info
- **Coordinate enrichment** via PropertyGuru Map Cluster API (lat/lng)
- **Robust** - handles site protection automatically
- **Cost-efficient** - works with datacenter proxy

### Quick Start

#### All properties for sale

```json
{
  "mode": "search",
  "listing_type": "sale",
  "max_pages": 5
}
```

#### Condos for rent by location

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

#### HDB flats with price filter

```json
{
  "mode": "search",
  "listing_type": "sale",
  "property_type": "hdb",
  "bedrooms": 3,
  "max_price": 500000
}
```

#### Landed houses by district

```json
{
  "mode": "search",
  "listing_type": "sale",
  "property_type": "landed",
  "district": "D10"
}
```

#### Custom URL mode

```json
{
  "mode": "url",
  "urls": ["https://www.propertyguru.com.sg/condo-for-sale?bedrooms=2&district_code=D09"],
  "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`, `hdb`, or `landed` |
| `search` | string | - | Free-text location search (e.g. "Orchard", "Tampines") |
| `district` | string | - | District code D01-D28 (see table below) |
| `min_price` | integer | - | Minimum price in SGD |
| `max_price` | integer | - | Maximum price in SGD |
| `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 | `100` | Max properties to scrape (0=unlimited) |
| `max_pages` | integer | `5` | Max search result pages (20 listings/page) |
| `enable_coordinates` | boolean | `false` | Add lat/lng to listings via Map API. Slows down the run (~30-60s extra). |
| `proxy` | object | Apify datacenter | 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**: `search` and `district` can be used together, but `search` takes priority in URL construction. `property_type` filters via URL slug; all other filters are query parameters 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 + filters + URLs + detail-page setting, 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.

### Singapore Districts

| Code | Area | Code | Area |
|------|------|------|------|
| D01 | Raffles Place / Marina | D15 | East Coast / Marine Parade |
| D02 | Chinatown / Tanjong Pagar | D16 | Bedok / Upper East Coast |
| D03 | Alexandra / Commonwealth | D17 | Changi Airport / Changi Village |
| D04 | Harbourfront / Telok Blangah | D18 | Pasir Ris / Tampines |
| D05 | Buona Vista / West Coast | D19 | Hougang / Punggol / Sengkang |
| D06 | City Hall / Clarke Quay | D20 | Ang Mo Kio / Bishan / Thomson |
| D07 | Beach Road / Bugis | D21 | Clementi Park / Upper Bukit Timah |
| D08 | Farrer Park / Serangoon Rd | D22 | Boon Lay / Jurong / Tuas |
| D09 | Orchard / River Valley | D23 | Bukit Batok / Bukit Panjang |
| D10 | Tanglin / Holland / Bukit Timah | D24 | Lim Chu Kang / Tengah |
| D11 | Newton / Novena | D25 | Admiralty / Woodlands |
| D12 | Balestier / Toa Payoh | D26 | Mandai / Upper Thomson |
| D13 | Macpherson / Potong Pasir | D27 | Sembawang / Yishun |
| D14 | Eunos / Geylang / Paya Lebar | D28 | Seletar / Yio Chu Kang |

### 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`, `price_psf`, `price_label` |
| **Features** | `bedrooms`, `bathrooms`, `floor_area`, `land_area`, `tenure` |
| **Property** | `property_type`, `property_type_group`, `badges` |
| **Location** | `district`, `district_code`, `region`, `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` |

`price_label` is the asking-price positioning label PropertyGuru shows beside the price (e.g. `Starting From`, `Negotiable`, `View to Offer`). It is read from the listing detail page, so it is only populated when detail enrichment is enabled. The field is always present on every record and is `null` when the listing carries no such label (or when detail enrichment is disabled).

# Actor input Schema

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

Search mode = build URLs from district/search/filters below. URL mode = paste any propertyguru.com.sg search URL already refined in a browser (recommended for complex filters).

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

URLs to scrape. Provide propertyguru.com.sg search or listing URLs. Tip: open propertyguru.com.sg 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. Note: commercial properties are on commercialguru.com.sg, not supported here.

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

Free-text location search (e.g. 'Orchard', 'Jurong', 'Tampines', 'Sentosa Cove'). Use this OR district code, not both.

## `district` (type: `string`):

Singapore district code (D01-D28). E.g. D09=Orchard, D10=Bukit Timah, D15=East Coast. Use this OR freetext search, not both.

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

Minimum price in Singapore Dollars

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

Maximum price in Singapore Dollars

## `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, TOP year, total units, facilities, floor plans, nearby schools, and virtual tours. 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 Singapore is required - PropertyGuru is geo-restricted to SG 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, TOP year, total\_units, facilities, nearby\_schools, coordinates, developer, tenure) from the first listing in each condo project and reuses them as a fallback if a later same-project listing's detail page fails to load. Every listing is still fetched individually, so per-listing fields such as price\_label, description and unit details are always captured accurately.

## `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 and detail-page 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 and detail-page 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.sg/property-for-sale?market=residential&property_type=N"
  ],
  "listing_type": "sale",
  "sort": "date",
  "sort_order": "desc",
  "enable_detail_pages": true,
  "max_properties": 10,
  "max_pages": 20,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "SG"
  },
  "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.sg/property-for-sale?market=residential&property_type=N"
    ],
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "SG"
    },
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/propertyguru-sg-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.sg/property-for-sale?market=residential&property_type=N"],
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "SG",
    },
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/propertyguru-sg-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.sg/property-for-sale?market=residential&property_type=N"
  ],
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "SG"
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false
}' |
apify call abotapi/propertyguru-sg-scraper --silent --output-dataset

```

## MCP server setup

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