CommercialGuru SG Property Listings & Agent Contacts Scraper
Pricing
from $1.00 / 1,000 results
CommercialGuru SG Property Listings & Agent Contacts Scraper
Scrape commercialguru.com.sg Singapore listings at scale. Extract prices, PSF, floor area, tenure, property type, images, agent contacts, coordinates, nearby MRT stations and more. Ideal for commercial real estate analytics, investment research and market intelligence.
Pricing
from $1.00 / 1,000 results
Rating
0.0
(0)
Developer
Abot API
Maintained by CommunityActor stats
0
Bookmarked
9
Total users
2
Monthly active users
21 days ago
Last modified
Share
CommercialGuru Singapore Scraper
Extract commercial property listings from commercialguru.com.sg (PropertyGuru Group) at scale. Get comprehensive data including prices, PSF, floor area, tenure, images, agent contacts, coordinates, nearby MRT, and more. Perfect for Singapore commercial property analytics, investment research, and market intelligence.
Key Features
| Feature | Description |
|---|---|
| All Listing Types | For Sale and For Rent commercial properties across Singapore |
| 11 Property Types | Office, Retail, Shop/Shophouse, Warehouse, Factory, Industrial, Land, F&B, Business Park, Dormitory |
| Price & Size Filters | Narrow results by price range (SGD) and floor area (sqft) |
| Location Filters | Filter by district (D01-D28) or freetext search |
| Exact Coordinates | Lat/lng enrichment via PropertyGuru Map Cluster API |
| Multiple Dataset Views | Overview, Map View, Agents & Developers |
| Pagination | Multi-page scraping with automatic retry on blocked pages |
What data can you extract?
The scraper extracts 25+ fields per listing:
|
Property Details
|
Pricing
|
|
Location & Transit
|
Agent & Developer
|
|
Media
|
Dates & Metadata
|
With fetch_details enabled, each listing also includes:
|
Description & Terms
|
Location & Contact
|
Coordinates are also added by the separate
enable_coordinatesoption (via the Map API).fetch_detailsis slower and uses more proxy traffic; both are off by default.
Incremental mode only. When incrementalMode is on, every returned record also carries:
| Field | Description |
|---|---|
changeType | NEW | UPDATED | UNCHANGED | REAPPEARED | EXPIRED |
changedFields | Top-level fields that changed since last seen; non-empty only for UPDATED |
firstSeenAt | When this listing was first observed by this monitoring campaign |
lastSeenAt | When this listing was last observed |
How to Use
Step 1: Search offices for rent in CBD
{"mode": "search","listing_type": "rent","property_type": "office","search": "Raffles Place","max_properties": 50,"max_pages": 5}
Step 2: Filter shops by district and price
{"mode": "search","listing_type": "sale","property_type": "shop","district": "D01","min_price": 500000,"max_price": 5000000,"max_properties": 100,"max_pages": 10}
Step 3: Warehouses by floor area
{"mode": "search","listing_type": "rent","property_type": "warehouse","min_floor_area": 5000,"max_floor_area": 20000,"sort": "price","sort_order": "asc"}
Step 4: Scrape specific URLs
{"mode": "url","urls": ["https://www.commercialguru.com.sg/office-for-rent?district_code=D01","https://www.commercialguru.com.sg/shop-for-sale?freetext=Orchard"],"max_properties": 50,"max_pages": 5}
Input Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
mode | string | search | search (build URL from filters) or url (use provided URLs) |
urls | array | [] | CommercialGuru search URLs (url mode only) |
listing_type | string | sale | sale or rent |
property_type | string | - | See Property Types table below |
locations | array | [] | One or more locations to search (e.g. ["Raffles Place", "Jurong"]). Each is searched separately and results are combined. Takes priority over search. |
search | string | - | Single free-text location search (e.g. "Raffles Place", "Jurong", "CBD") |
district | string | - | District code D01-D28 (see table below) |
min_price | integer | - | Minimum price in SGD |
max_price | integer | - | Maximum price in SGD |
min_floor_area | integer | - | Minimum floor area in sqft |
max_floor_area | integer | - | Maximum floor area in sqft |
sort | string | date | Sort by: date, price, psf, area (floor area) |
sort_order | string | desc | asc or desc |
max_properties | integer | 10 | Max listings 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). |
fetch_details | boolean | false | Adds extra fields per listing: full description, agent phone, exact coordinates, postal code, full image gallery, nearby MRT & schools, lease term, condition, amenities, and floor level. Slower and uses more proxy traffic. |
resumeFromRunId | string | none | Continue one specific previous run/dataset: properties already collected there are skipped. For recurring monitoring, use Incremental mode instead — see "Resume & recurring updates" below. |
incrementalMode | boolean | false | Daily/recurring monitoring of this same search. First run returns everything as NEW; later runs return only NEW/UPDATED/REAPPEARED by default. See "Resume & recurring updates" below. |
stateKey | string | none | Incremental mode only. Name this monitoring campaign, or leave empty to derive one automatically from the search/filter settings. |
emitUnchanged | boolean | false | Incremental mode only. Also return properties unchanged since the last run, marked UNCHANGED. Adds and bills extra rows you already have. |
emitExpired | boolean | false | Incremental mode only. Also return properties from a previous run no longer found, marked EXPIRED, once a run has fully scanned the search (not capped, not a resume). Adds and bills extra synthetic rows. |
proxy | object | Apify residential | Proxy configuration (Apify proxy recommended) |
dataset_name | string | default | Named dataset for results |
clear_dataset | boolean | false | Clear dataset before scraping |
Property Types
| Code | Type | Description |
|---|---|---|
office | Office | Office spaces |
retail | All Retail | All retail properties (shops, F&B, etc.) |
shop | Shop / Shophouse | Shops and shophouses |
shophouse | Shophouse | Shophouses specifically |
warehouse | Warehouse | Warehouse spaces |
factory | Factory / Industrial | All factory and industrial properties |
industrial | All Industrial | All industrial types (factory, warehouse, dormitory) |
land | Commercial Land | Commercial land plots |
food_beverage | Food & Beverage | F&B outlets and restaurants |
business_park | Business / Science Park | Business and science park spaces |
dormitory | Dormitory | Worker dormitories |
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 |
Resume & recurring updates
There are two different things here — pick the one that matches what you're doing:
| Need | Use |
|---|---|
| A crawl stopped and should continue | resumeFromRunId |
| Run the same search every day and receive only changes | incrementalMode |
| Keep separate daily campaigns for similar searches | distinct stateKey values |
| Run a normal full snapshot | leave both off |
Resume (resumeFromRunId) continues one specific interrupted or previous large crawl: paste a run ID or dataset ID and this run skips properties already collected there, returning only the remaining new ones.
Incremental mode (incrementalMode) is for a schedule (for example, daily): the actor remembers the previous run of the same search by itself, so you never paste a run ID. The first run returns everything as NEW. Later runs return only NEW, UPDATED, and REAPPEARED properties by default — duplicates and unchanged properties are suppressed (never pushed, never billed). Turn on emitUnchanged or emitExpired only when you also want those rows returned (and billed for). State is isolated per search/URL and filter/fetch_details/enable_coordinates setup automatically; set stateKey to name or deliberately share a monitoring campaign.
Identity is tracked by id — commercialguru.com.sg's own listing id (every parsed listing has one; a listing without an id is dropped before it reaches change detection). One field is excluded from change detection: recency (e.g. "Listed on Aug 03, 2026 (2h ago)") is a relative-age string the site renders at request time, so it drifts with elapsed time alone, not real content — it is still returned in full on every row, just never used to decide UPDATED. posted_date/posted_unix (the real "when was this posted" fields) stay in change detection.
If fetch_details is on and a listing's own detail-page fetch fails on a given run (a transient block, not every listing on the page), that listing's detail-only fields are treated as unknown for this run, not removed — the actor carries the last successful detail snapshot forward before comparing, so a transient per-listing failure is reported UNCHANGED and never bills, and recovers cleanly once the fetch succeeds again. A genuine change to any field the fetch DID return (including price) is still reported normally.
Scheduled-run example — same search, run daily:
Day 1 (first run ever for this search):
{ "mode": "search", "listing_type": "sale", "district": "D01", "incrementalMode": true }
→ every listing comes back with "changeType": "NEW".
Day 2 (the schedule fires again, identical input):
{ "mode": "search", "listing_type": "sale", "district": "D01", "incrementalMode": true }
→ listings whose price/tenure/etc. changed come back as "changeType": "UPDATED" with changedFields listing what changed, brand-new listings come back as "changeType": "NEW", listings that vanished and came back come back as "changeType": "REAPPEARED" — and listings that are still there, unchanged, are not returned at all (suppressed, not charged) unless emitUnchanged is on. EXPIRED tombstones for listings no longer found are only produced when emitExpired is on and the run fully scanned the tracked search (not capped by Max properties, not a resume).
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.
- Authorize a connector once under Apify → Settings → Integrations (Notion, Linear, Airtable, or Apify).
- Select it in the "Pipe results into your apps" input field. (If the picker is empty, you haven't authorized a connector yet.)
- For Notion, also set
notionParentPageUrlto 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 Example
Commercial property listing
{"id": 500073292,"title": "Novena Medical Center","address": "10 Sample Drive","listing_type": "sale","property_type": "Office","property_type_group": "O","price": 3800000,"price_formatted": "S$ 3,800,000","currency": "SGD","price_psf": "S$ 4,973.82 psf","floor_area": 764,"tenure": "99-year Leasehold","agent_name": "Sample","agent_id": 1758000,"agent_license": "R0123000","agency_name": "Sample PTE LTD","district": "Newton / Novena","district_code": "D11","region": "Central (D09-11)","nearby_mrt": "2 min (170 m) from NS20 Novena MRT Station","latitude": 1.3000,"longitude": 103.000,"location_source": "map_api","badges": ["Office", "99-year Leasehold"],"posted_date": "2 Apr 2026","posted_unix": 177508000,"recency": "Listed on Apr 02, 2026 (30m ago)","images": ["https://sg1-cdn.pgimgs.com/listing/500073292/UPHO.123456.V800/Novena-Sample.jpg"],"image_count": 1,"url": "https://www.commercialguru.com.sg/listing/for-sale-novena-medical-center-Sample","status": "ACT"}
Additional fields with fetch_details enabled
{"description": "Rare freehold ground-floor medical suite.\nFully fitted, ready to move in.\nHigh footfall, wide frontage.","description_headline": "Freehold medical suite, fully fitted","postal_code": "307506","latitude": 1.3201,"longitude": 103.8439,"location_source": "detail_page","agent_phone": "+6580001234","agent_title": "Associate Group Director","floor_level": "Ground Floor","rental_type": "Entire Unit","unit_condition": "Fully Fitted","unit_features": ["Air Conditioner"],"lease_term": "99 years","amenities": ["Air conditioner"],"nearby_mrt_stations": [{ "name": "NS20 Novena MRT Station", "distance_km": 0.17, "walk_minutes": 2 }],"nearby_schools": [{ "name": "Sample Primary School", "distance_km": 0.45, "walk_minutes": 6 }],"available_date": "2026-06-15 00:00:00","detail_fetched": true}