CommercialGuru SG Property Listings & Agent Contacts Scraper avatar

CommercialGuru SG Property Listings & Agent Contacts Scraper

Pricing

from $1.00 / 1,000 results

Go to Apify Store
CommercialGuru SG Property Listings & Agent Contacts Scraper

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

Abot API

Maintained by Community

Actor 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

FeatureDescription
All Listing TypesFor Sale and For Rent commercial properties across Singapore
11 Property TypesOffice, Retail, Shop/Shophouse, Warehouse, Factory, Industrial, Land, F&B, Business Park, Dormitory
Price & Size FiltersNarrow results by price range (SGD) and floor area (sqft)
Location FiltersFilter by district (D01-D28) or freetext search
Exact CoordinatesLat/lng enrichment via PropertyGuru Map Cluster API
Multiple Dataset ViewsOverview, Map View, Agents & Developers
PaginationMulti-page scraping with automatic retry on blocked pages

What data can you extract?

The scraper extracts 25+ fields per listing:

Property Details

  • Listing ID & URL
  • Title & full address
  • Property type (Office, Retail, Shophouse, etc.)
  • Floor area & land area (sqft)
  • Tenure (Freehold/Leasehold)
  • Badges (unit type, tenure, year built, etc.)

Pricing

  • Price (SGD numeric value)
  • Formatted price (e.g. "S$ 3,800,000")
  • Price per sqft (PSF)
  • Listing type (Sale/Rent)

Location & Transit

  • Latitude & longitude (from Map Cluster API)
  • District name & code (D01-D28)
  • Region grouping
  • Nearest MRT station with distance

Agent & Developer

  • Agent name, ID & CEA license
  • Agency name
  • Developer name
  • Is developer listing flag

Media

  • Property images (up to 10 per listing)
  • Image count

Dates & Metadata

  • Posted date (text & unix timestamp)
  • Recency text
  • Listing status (Active, etc.)

With fetch_details enabled, each listing also includes:

Description & Terms

  • Full description & headline
  • Lease term (e.g. "2 years")
  • Rental type (e.g. Entire Unit)
  • Condition (e.g. Fully Fitted)
  • Unit features & amenities
  • Floor level
  • Available-from date

Location & Contact

  • Exact coordinates (from the listing page)
  • Postal code
  • Agent phone (mobile) & title
  • Nearby MRT stations (with distance & walk time)
  • Nearby schools
  • Full image gallery (beyond the 10-image card cap)

Coordinates are also added by the separate enable_coordinates option (via the Map API). fetch_details is slower and uses more proxy traffic; both are off by default.

Incremental mode only. When incrementalMode is on, every returned record also carries:

FieldDescription
changeTypeNEW | UPDATED | UNCHANGED | REAPPEARED | EXPIRED
changedFieldsTop-level fields that changed since last seen; non-empty only for UPDATED
firstSeenAtWhen this listing was first observed by this monitoring campaign
lastSeenAtWhen 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

ParameterTypeDefaultDescription
modestringsearchsearch (build URL from filters) or url (use provided URLs)
urlsarray[]CommercialGuru search URLs (url mode only)
listing_typestringsalesale or rent
property_typestring-See Property Types table below
locationsarray[]One or more locations to search (e.g. ["Raffles Place", "Jurong"]). Each is searched separately and results are combined. Takes priority over search.
searchstring-Single free-text location search (e.g. "Raffles Place", "Jurong", "CBD")
districtstring-District code D01-D28 (see table below)
min_priceinteger-Minimum price in SGD
max_priceinteger-Maximum price in SGD
min_floor_areainteger-Minimum floor area in sqft
max_floor_areainteger-Maximum floor area in sqft
sortstringdateSort by: date, price, psf, area (floor area)
sort_orderstringdescasc or desc
max_propertiesinteger10Max listings to scrape (0 = unlimited)
max_pagesinteger5Max search result pages (20 listings/page)
enable_coordinatesbooleanfalseAdd lat/lng to listings via Map API. Slows down the run (~30-60s extra).
fetch_detailsbooleanfalseAdds 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.
resumeFromRunIdstringnoneContinue one specific previous run/dataset: properties already collected there are skipped. For recurring monitoring, use Incremental mode instead — see "Resume & recurring updates" below.
incrementalModebooleanfalseDaily/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.
stateKeystringnoneIncremental mode only. Name this monitoring campaign, or leave empty to derive one automatically from the search/filter settings.
emitUnchangedbooleanfalseIncremental mode only. Also return properties unchanged since the last run, marked UNCHANGED. Adds and bills extra rows you already have.
emitExpiredbooleanfalseIncremental 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.
proxyobjectApify residentialProxy configuration (Apify proxy recommended)
dataset_namestringdefaultNamed dataset for results
clear_datasetbooleanfalseClear dataset before scraping

Property Types

CodeTypeDescription
officeOfficeOffice spaces
retailAll RetailAll retail properties (shops, F&B, etc.)
shopShop / ShophouseShops and shophouses
shophouseShophouseShophouses specifically
warehouseWarehouseWarehouse spaces
factoryFactory / IndustrialAll factory and industrial properties
industrialAll IndustrialAll industrial types (factory, warehouse, dormitory)
landCommercial LandCommercial land plots
food_beverageFood & BeverageF&B outlets and restaurants
business_parkBusiness / Science ParkBusiness and science park spaces
dormitoryDormitoryWorker dormitories

Singapore Districts

CodeAreaCodeArea
D01Raffles Place / MarinaD15East Coast / Marine Parade
D02Chinatown / Tanjong PagarD16Bedok / Upper East Coast
D03Alexandra / CommonwealthD17Changi Airport / Changi Village
D04Harbourfront / Telok BlangahD18Pasir Ris / Tampines
D05Buona Vista / West CoastD19Hougang / Punggol / Sengkang
D06City Hall / Clarke QuayD20Ang Mo Kio / Bishan / Thomson
D07Beach Road / BugisD21Clementi Park / Upper Bukit Timah
D08Farrer Park / Serangoon RdD22Boon Lay / Jurong / Tuas
D09Orchard / River ValleyD23Bukit Batok / Bukit Panjang
D10Tanglin / Holland / Bukit TimahD24Lim Chu Kang / Tengah
D11Newton / NovenaD25Admiralty / Woodlands
D12Balestier / Toa PayohD26Mandai / Upper Thomson
D13Macpherson / Potong PasirD27Sembawang / Yishun
D14Eunos / Geylang / Paya LebarD28Seletar / Yio Chu Kang

Resume & recurring updates

There are two different things here — pick the one that matches what you're doing:

NeedUse
A crawl stopped and should continueresumeFromRunId
Run the same search every day and receive only changesincrementalMode
Keep separate daily campaigns for similar searchesdistinct stateKey values
Run a normal full snapshotleave 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.

  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 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
}