99.co Singapore Scraper
Under maintenancePricing
from $1.20 / 1,000 dataset items
99.co Singapore Scraper
Under maintenanceScrape Singapore property listings from 99.co for sale and rent. Prefer Start URLs copied from the site, or search by location with the same filters as the website (condo/HDB/landed, price, size, tenure). Optionally enrich with listing details and agent profiles.
Pricing
from $1.20 / 1,000 dataset items
Rating
0.0
(0)
Developer
Farhan Ali
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
a day ago
Last modified
Categories
Share
99.co Singapore Scraper creates a structured dataset of property listings collected from 99.co in Singapore. Each dataset item is one sale or rent listing and can include asking price in Singapore dollars, price per square foot, floor area, bedrooms, tenure, neighbourhood, and the listing agent. Query with a 99.co search, listing, or agent URL, or a location such as Orchard, and set listing type, segment, price, size, and bedroom filters. Control the result limit with maxItems. Retrieve records through the Apify Dataset API or export them as JSON, CSV, Excel, XML, or another supported format.
Dataset at a glance
| Property | Value |
|---|---|
| Source | 99.co (www.99.co), Singapore |
| Record unit | One property listing |
| Input methods | Start URL (search, listing, or agent) or a location search query |
| Main identifiers | id, url |
| Delivery | Apify Dataset and API |
| Export formats | JSON, CSV, Excel, XML, and other Apify dataset exports |
| Update model | Fresh records per Actor run |
| Pricing | Pay per event: dataset-item $0.001; optional listing-details and agent-info $0.001 each |
Coverage and available records
The Actor collects public 99.co sale and rent listings. A start URL is used as copied from the browser.
- A search URL is paginated as that search. Filters in the input apply to search queries, not to a pasted search URL.
- A listing URL returns that listing.
- An agent URL collects listings from that agent profile.
searchQueriesaccepts locations or keywords such as Orchard, Tampines, District 9, or Punggol MRT. With no start URL and no query, the Actor runs one search for the selected listing type and filters without a location string.listingTypeis sale or rent. It is ignored when the start URL already contains /sale or /rent.propertySegmentsaccepts residential, commercial, industrial, or land. The stored sample is residential.- Property group and subtype filters accept the condo, HDB, and landed values in the input schema. Landed subtypes are sent as a landed subtype; other subtypes are sent as a subcategory.
- Price bounds are Singapore dollars. For sale they are the total asking price. For rent they are monthly rent.
- Bedroom 0 means studio and bedroom 5 means 5 or more. Bathroom 5 means 5 or more.
- Floor size is in square feet. Tenure accepts Freehold, 99 yrs, 999 yrs, and 103 yrs. Furnishing accepts unfurnished, partial, and fully.
rentalTypeapplies to rent searches: entire unit or room.newLaunchlimits the search to new-launch projects.- Sort options are relevance, newest, price, price per square foot, and size.
maxItemsof 0 does not cap the result count. A positive value stops after that many saved listings.scrapeListingDetailsopens the listing page and, when parsing succeeds, merges description, photos, coordinates, and unit fields onto the same record. That adds alisting-detailscharge.scrapeAgentInfoopens the agent profile and, when parsing succeeds, adds anagentobject plus agency fields. That adds anagent-infocharge.- The run requires a proxy configuration. The input default is Apify residential with country SG. Without a proxy, the Actor stops before scraping.
Search-card records omit keys whose values are empty. Detail and agent fields below are conditional. They were not present on the five stored search-card records. A blocked listing page is skipped for enrichment; the card record can still be saved. The Actor does not keep a historical archive.
Data dictionary
Fields below are the keys observed on stored search-card records. Types and the example values come from those records. A key is nullable when at least one stored record omitted it or set it to null.
| Field | Type | Nullable | Description | Example |
|---|---|---|---|---|
id | string | No | 99.co listing id, also the last segment of the listing URL. | npntbSGVfMgek7jRqgc2yd |
url | string | No | Absolute listing URL on www.99.co. | https://www.99.co/singapore/sale/property/eight-riversuites-condo-npn... |
title | string | No | Project or address title shown for the listing. | Eight Riversuites |
listingType | string | No | Source listing type. Observed value: sale. | sale |
listingTypeText | string | No | Label derived by the Actor. sale becomes Buy; rent becomes Rent. | Buy |
price | number | No | Asking price as a number. Sale prices are the total; rent prices are monthly. | 1299999.0 |
priceText | string | No | Formatted asking price from the source, including the S$ symbol. | S$ 1,299,999 |
currency | string | No | Currency code set by the Actor. Observed value: SGD. | SGD |
psf | number | No | Price per square foot as a number. | 1857.0 |
psfText | string | No | Formatted price per square foot from the source. | S$ 1,857 psf |
bedrooms | integer | No | Bedroom count. Studio listings use 0 when the source sends it. | 2 |
bathrooms | integer | No | Bathroom count. | 2 |
floorAreaSqft | number | No | Internal floor area in square feet. | 700.0 |
floorAreaText | string | No | Formatted floor area from the source, such as 517 sqft. | 700 sqft |
propertyType | string | No | Display property type, such as Condo or HDB 5 Rooms. | Condo |
propertyTypeCode | string | No | Source subtype code, such as generic_condo. | generic_condo |
propertyTypeGroup | string | No | Source type group, such as condo. | condo |
propertyName | string | No | Project or building name. | Eight Riversuites |
address | string | No | Street address line from the card, including the postal code when the source includes it. | 8 Whampoa East 338520 |
neighbourhood | string | No | Neighbourhood name. Omitted on some cards. | Bendemeer |
region | string | No | Planning region, such as North Region. Omitted on some cards. | Central Region |
tenure | string | No | Tenure text, such as 99 yrs. | 99 yrs |
builtYear | integer | No | Year the project was built, when the card includes it. | 2016 |
builtYearText | string | No | Formatted built-year text from the source. | Built: 2016 |
latitude | number | No | Listing latitude in decimal degrees. | 1.3216293792249243 |
longitude | number | No | Listing longitude in decimal degrees. | 103.86445663693168 |
photoCount | integer | No | Photo count from the card. | 8 |
floorPlanCount | integer | No | Floor-plan count from the card. | 1 |
videoCount | integer | No | Video count from the card. | 0 |
listedOnText | string | No | Relative listed-time text from the source, such as 1s. | 1s |
postedAt | integer | No | Source posted-time integer, copied as returned. | 1786701960252279 |
isMustSee | boolean | No | True when the card is marked Must See. | true |
mrt | string | No | Nearest MRT label from the card. Omitted when absent. | Boon Keng MRT |
statusCode | string | No | Source status code, such as must_see. | must_see |
propertySegment | string | No | Source segment. Observed value: residential. | residential |
sourceUrl | string | No | Start URL or search URL that produced the record. | https://www.99.co/singapore/sale |
scrapedAt | string | No | UTC timestamp written by the Actor when the record is saved, ISO-8601. | 2026-08-14T10:07:15.948796+00:00 |
thumbnailUrl | string | Yes | Card image URL. Omitted when the card has no photo. | https://pic2.99.co/v3/VeBpwpwaaUryeAcLzouyTf?sampling=lanczos&version... |
agentId | string | Yes | Agent id from the card. Omitted when the card has no agent id. | ovaEwyAwFz6S4ypZYdXJW6 |
agentName | string | Yes | Agent name from the card. | Thiru P |
agentPhone | string | Yes | Agent phone from the card. | +6591847685 |
agentWhatsapp | string | Yes | WhatsApp number from the card. | +6591847685 |
agentIsPremium | boolean | Yes | True when the card marks the agent as premium. | true |
Listing-detail fields
These keys are produced by the listing-page parser when scrapeListingDetails is true and the page parses. Empty values are omitted. They are not in the stored search-card sample, so the example column is not filled from that sample.
| Field | Type | Nullable | Description | Example |
|---|---|---|---|---|
headline | string | Yes | Listing subtitle from the detail page. | Not in the stored sample |
description | string | Yes | Meta description text from the listing page. | Not in the stored sample |
floorAreaSqm | number | Yes | Floor area in square metres when the card payload includes it. | Not in the stored sample |
landAreaSqft | number | Yes | Land area in square feet. | Not in the stored sample |
streetName | string | Yes | Street name. | Not in the stored sample |
streetNumber | string | Yes | Street number. | Not in the stored sample |
postalCode | string | Yes | Postal code. | Not in the stored sample |
districtNumber | integer | Yes | District number. | Not in the stored sample |
furnishing | string | Yes | Furnishing text from the listing payload. | Not in the stored sample |
imageUrls | array | Yes | Photo URLs. Detail pages keep up to 30 pic2.99.co images. | Not in the stored sample |
availableFrom | string | Yes | Availability text from the listing. | Not in the stored sample |
unitConfiguration | string | Yes | Unit configuration text. | Not in the stored sample |
isVerified | boolean | Yes | Must-see verified flag. | Not in the stored sample |
isOwnerListing | boolean | Yes | True when the detail payload marks an owner listing. | Not in the stored sample |
isNewListing | boolean | Yes | True when the detail payload marks a new listing. | Not in the stored sample |
isNewLaunch | boolean | Yes | True when the detail payload marks a new launch. | Not in the stored sample |
agentLicense | string | Yes | CEA number from the listing or agent block. | Not in the stored sample |
agentUrl | string | Yes | Agent profile URL built from the CEA number and name. | Not in the stored sample |
agentPhotoUrl | string | Yes | Agent photo URL. | Not in the stored sample |
clusterId | string | Yes | Cluster id from the detail payload. | Not in the stored sample |
agencyName | string | Yes | Agency name, filled from the agent profile when that profile is fetched. | Not in the stored sample |
agencyLicense | string | Yes | Agency license number from the agent profile. | Not in the stored sample |
Agent profile object
When scrapeAgentInfo succeeds, the record gains an agent object. The parser omits empty keys. Nested list and object values are copied from the profile payload. No stored sample includes this object.
| Field | Type | Nullable | Description | Example |
|---|---|---|---|---|
id | string | Yes | CEA agent number. | Not in the stored sample |
name | string | Yes | Agent name. | Not in the stored sample |
license | string | Yes | Same CEA number as id. | Not in the stored sample |
jobTitle | string | Yes | Job title. | Not in the stored sample |
phone | string | Yes | Raw or masked phone from the profile. | Not in the stored sample |
email | string | Yes | Email from the profile. | Not in the stored sample |
photoUrl | string | Yes | Profile photo URL. | Not in the stored sample |
description | string | Yes | Profile biography. | Not in the stored sample |
agencyName | string | Yes | Agency name. | Not in the stored sample |
agencyLicense | string | Yes | Agency number. | Not in the stored sample |
agencyLogoUrl | string | Yes | Agency logo URL. | Not in the stored sample |
areaExpertise | array | Yes | Area expertise list from the profile. | Not in the stored sample |
listingsBreakdown | object | Yes | Listings breakdown object from the profile. | Not in the stored sample |
transactionHistory | object | Yes | Transaction-history widget from the profile. | Not in the stored sample |
isCorporate | boolean | Yes | Corporate-agent flag. | Not in the stored sample |
profileUrl | string | Yes | Agent profile URL. | Not in the stored sample |
Use id as the deduplication key for a listing. It is stable on the listing URL.
Example dataset record
The record below is a stored search-card item from a sale search at https://www.99.co/singapore/sale. scrapeListingDetails and scrapeAgentInfo were not required for these fields. Empty keys were omitted by the parser.
{"id": "eAxBcoKvWVRPCSa9yo7GwS","url": "https://www.99.co/singapore/sale/property/the-miltonia-residences-condo-eAxBcoKvWVRPCSa9yo7GwS","title": "The Miltonia Residences","listingType": "sale","listingTypeText": "Buy","price": 695000.0,"priceText": "S$ 695,000","currency": "SGD","psf": 1344.0,"psfText": "S$ 1,344 psf","bedrooms": 1,"bathrooms": 1,"floorAreaSqft": 517.0,"floorAreaText": "517 sqft","propertyType": "Condo","propertyTypeCode": "generic_condo","propertyTypeGroup": "condo","propertyName": "The Miltonia Residences","address": "520 Miltonia Close 768104","neighbourhood": "Lower Seletar","region": "North Region","tenure": "99 yrs","builtYear": 2015,"builtYearText": "Built: 2015","latitude": 1.4170849196563045,"longitude": 103.84818889236466,"thumbnailUrl": "https://pic2.99.co/v3/VeBpwpwaaUryeAcLzouyTf?sampling=lanczos&version=1&height=120&width=120&mode=fill&quality=70&signature=fdadf666f0f6c2ca06589849254bf4893e60316d","photoCount": 16,"floorPlanCount": 1,"videoCount": 0,"listedOnText": "1s","postedAt": 1786701960962578,"isMustSee": true,"mrt": "Khatib MRT","agentId": "ovaEwyAwFz6S4ypZYdXJW6","agentName": "Thiru P","agentPhone": "+6591847685","agentWhatsapp": "+6591847685","agentIsPremium": true,"statusCode": "must_see","propertySegment": "residential","sourceUrl": "https://www.99.co/singapore/sale","scrapedAt": "2026-08-14T10:07:16.672060+00:00"}
Query and input reference
| Input | Type | Required | Default | Accepted values | Description |
|---|---|---|---|---|---|
startUrls | array | No | — | 99.co search, listing, or agent URLs | Preferred source. Paginated as copied. Filters below do not rewrite these URLs. |
searchQueries | array of strings | No | — | Location or keyword text | Mapped to the 99.co name search. Example: Orchard. |
listingType | string | No | sale | sale, rent | Buy or rent. Ignored when the start URL already contains /sale or /rent. |
propertySegments | string | No | residential | residential, commercial, industrial, land | Property segment. |
mainCategory | array of strings | No | — | condo, hdb, landed | High-level type group. Empty means all, or the groups implied by subtypes. |
propertySubTypes | array of strings | No | — | Condo, HDB room counts, and landed subtypes in the input schema | Specific types. Landed values are sent as landed subtype. |
minPrice | integer | No | — | 0 or greater, SGD | Minimum asking price. Sale is the total. Rent is monthly. |
maxPrice | integer | No | — | 0 or greater, SGD | Maximum asking price. |
bedrooms | array of strings | No | — | 0, 1, 2, 3, 4, 5 | 0 is studio. 5 is 5 or more. |
bathrooms | array of strings | No | — | 1, 2, 3, 4, 5 | 5 is 5 or more. |
minSize | integer | No | — | 0 or greater | Minimum floor area in square feet. |
maxSize | integer | No | — | 0 or greater | Maximum floor area in square feet. |
leaseType | array of strings | No | — | Freehold, 99 yrs, 999 yrs, 103 yrs | Tenure filter. |
furnishing | array of strings | No | — | unfurnished, partial, fully | Furnishing filter. |
rentalType | string | No | empty | empty, all, unit, room | Rent searches only. Entire unit or room. |
newLaunch | boolean | No | false | true, false | New-launch projects only when true. |
sortBy | string | No | relevance | relevance, newest, price_asc, price_desc, psf_asc, psf_desc, size_desc, size_asc | Search result order. |
scrapeListingDetails | boolean | No | false | true, false | Merge listing-page fields. Charges listing-details when details are attached. |
scrapeAgentInfo | boolean | No | false | true, false | Attach the agent profile. Charges agent-info when agent data is attached. |
maxItems | integer | No | 0 | 0 or greater | Maximum listings to save. 0 means no cap. |
maxConcurrency | integer | No | 5 | 1 to 8 | Parallel listing and agent page workers. |
proxyConfiguration | object | No | Apify residential, country SG | Apify proxy input | Required for a run. The Actor returns before scraping when no proxy is configured. |
Minimal request:
{"searchQueries": ["Orchard"],"listingType": "sale","maxItems": 20}
Request with listing-page and agent enrichment:
{"startUrls": [{"url": "https://www.99.co/singapore/sale"}],"listingType": "sale","mainCategory": ["condo"],"bedrooms": ["2", "3"],"maxItems": 50,"scrapeListingDetails": true,"scrapeAgentInfo": true,"maxConcurrency": 5,"proxyConfiguration": {"useApifyProxy": true,"apifyProxyGroups": ["RESIDENTIAL"],"apifyProxyCountry": "SG"}}
Retrieve the data through the API
This is the Apify Actor API. 99.co does not provide this endpoint.
- Start
datascrapers/99-co-scraperwith a JSON input. - Wait until the run finishes, or poll the run status.
- Read items from the run's default dataset.
- Page the dataset with offset and limit, or export it from the run page.
from apify_client import ApifyClientclient = ApifyClient("YOUR_API_TOKEN")run = client.actor("datascrapers/99-co-scraper").call(run_input={"searchQueries": ["Orchard"],"listingType": "sale","maxItems": 20,})items = client.dataset(run["defaultDatasetId"]).list_items().itemsprint(items)
Other languages are listed on the Actor's API tab in Apify Console. Do not put a real token in a URL or in source control.
Export and pipeline examples
| Destination | Recommended method | Typical use |
|---|---|---|
| PostgreSQL or Supabase | Dataset API or a webhook consumer | Store id as the listing key |
| Google Sheets | Apify integration | Share a price and area extract |
| Amazon S3 or other cloud storage | Scheduled dataset export or an integration | Archive each run's JSON or CSV |
| A custom application | Dataset items endpoint | Page through a large search |
Data quality and record handling
Search-card fields come from the 99.co listing payload. currency is set to SGD by the Actor. listingTypeText is derived from listingType. Other card fields are copied from the source. Empty strings, empty lists, and nulls are dropped before the card is saved, so a missing key means the source value was empty or the card did not include it.
Within one run, discovery skips a listing id that was already yielded. The recommended key across runs is id. url identifies the same listing page.
listing-details is charged only when a listing page is merged onto the record. agent-info is charged only when an agent profile is attached. A failed detail or agent request does not remove the card. A listing with neither id nor url is not saved.
postedAt is the source integer as returned. scrapedAt is the Actor's UTC save time. listedOnText is relative text from the page, not a normalized date. Coordinates are decimal degrees when the card includes them.
The stored sample does not include commercial, industrial, land, or rent rows. Those segments are accepted by the input. Field presence still depends on what 99.co returns for that search.
Pricing and cost examples
The Actor uses pay per event. Every saved listing charges one dataset-item event at $0.001. A listing that also receives parsed listing-page fields charges one listing-details event at $0.001. A listing that also receives an agent profile charges one agent-info event at $0.001. The same prices apply on every plan tier. There is no separate Actor-start event in this Actor.
Platform usage, including compute and residential proxy traffic, is billed to the run account in addition to these events. Estimates below are event charges only.
| Records | Events | Estimated event cost |
|---|---|---|
| 1,000 listing cards | 1,000 × dataset-item | $1.00 |
| 1,000 cards with details and agent profiles | 1,000 × each of the three events | $3.00 |
| 10,000 listing cards | 10,000 × dataset-item | $10.00 |
| 10,000 cards with details and agent profiles | 10,000 × each of the three events | $30.00 |
A listing that is not saved does not create a dataset-item charge. A detail or agent request that adds nothing does not create that extra charge. maxItems caps saved listings and therefore the dataset-item count.
Limitations and responsible data use
The Actor reads publicly available 99.co pages and the listing fields those pages expose. It does not return a historical archive. Each run reflects the site at the time of the request. Save datasets yourself if you need a time series.
Values can be missing when 99.co omits them, when a filter matches nothing, or when a detail or agent page does not parse. A residential proxy in Singapore is required for the run to start scraping. Site markup and availability can change.
Agent phone numbers and similar contact fields are present only when the listing or profile exposes them. You are responsible for using the dataset in line with 99.co terms, privacy rules, and any contract that applies to your project. This page does not provide a legal conclusion about a particular use.
Dataset questions
What does one dataset item represent?
One 99.co listing. Search-card runs contain the fields in the main dictionary. Detail and agent runs add the conditional fields on that same listing record.
Which field should I use as a unique identifier?
Use id. It is the listing id on the 99.co URL. url identifies the same page.
Are fields nullable or conditional?
Card keys are omitted when the source value is empty. Description, postal code, district, furnishing, and the agent object are conditional on the enrichment switches and on a successful page parse.
Can I retrieve the records as CSV or JSON?
Yes. Download JSON, CSV, Excel, XML, or another format from the Apify Console export menu, or read JSON through the Dataset API.
How do I paginate large datasets?
The Actor pages through a 99.co search until maxItems or the search ends. After the run, page the Apify dataset with offset and limit.
Does the Actor return historical data?
No. A record is the listing state observed during that run. Earlier prices are available only if you stored an earlier dataset.
What counts as a billable result?
Each saved listing is one dataset-item event at $0.001. Each listing whose detail page is merged is an additional listing-details event at $0.001. Each listing that receives an agent profile is an additional agent-info event at $0.001. Platform usage is charged separately.
Related datasets from Data Scrapers
- Property Finder Scraper — property listing records from Property Finder for a different market.
- Bayut Property Scraper — Bayut listing records with price and location fields.
- Dubizzle UAE Scraper — Dubizzle UAE classifieds, including property listings.
- Rightmove Scraper — UK sale and rent listing records for cross-market comparison.
- Realtor.com Scraper — US listing records with price and property attributes.
- Idealista Scraper — Spain and nearby-market listing records.
Data Scrapers support
Need an additional field, record type, or export workflow? Contact Data Scrapers at stardustspotlight@gmail.com. Include a sample source URL, required fields, expected record volume, and preferred delivery format.
