# 99.co Project & Condo Profile Scraper (`incognito_mode/99co-project-profile-scraper`) Actor

Scrape Singapore condo, HDB and landed project profiles from 99.co: tenure, TOP year, unit count, blocks, asking and transacted PSF, price range per bedroom type, owner/tenant mix, and nearby MRT, schools and amenities with walk, drive and taxi times.

- **URL**: https://apify.com/incognito\_mode/99co-project-profile-scraper.md
- **Developed by:** [Elena Vance](https://apify.com/incognito_mode) (community)
- **Categories:** Real estate, Business, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $20.00 / 1,000 project profiles

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## 99.co Project & Condo Profile Scraper

Scrape Singapore **development profiles** from [99.co](https://www.99.co) —
condos, HDB blocks and landed estates. Tenure, TOP year, unit count, blocks and
postal codes, asking and **transacted** price statistics, price range per
bedroom type, owner/tenant mix, and every nearby MRT, school, supermarket and
clinic with walking, driving, transit and taxi-fare estimates.

One row per project. No API key, no login, no residential proxy, no browser.

### What you get

A real sample, from a District 15 condo run:

| project | type | tenure | TOP | units | active listings | sold PSF (6m) | nearest MRT |
| --- | --- | --- | ---: | ---: | ---: | ---: | --- |
| Tembusu Grand | Condo | 99 years | 2025 | 638 | 229 | 2,734.87 | Tanjong Katong |
| Neptune Court | Condo | 99 years | 1975 | 752 | 30 | 1,073.29 | Marine Terrace |
| The Meyerise | Condo | Freehold | 2016 | 239 | 25 | 2,771.70 | Katong Park |
| Parc Elegance | Condo | Freehold | 2013 | 161 | 14 | 1,615.24 | Marine Parade |

### Transacted prices, not just asking prices

This is the part nothing else in 99.co's public API gives you. Each condo
profile carries a market-summary paragraph that this Actor parses into numbers:

| Field | Meaning |
| --- | --- |
| `soldAvgPsf6Months` / `soldAvgPsf12Months` | **Transacted** average price per sqft |
| `latestSalePrice` | Most recent recorded transaction |
| `highestSalePrice` | Highest recorded transaction |
| `askingPriceMin` / `askingPriceMax` | Current asking range |
| `rentAvgPsf6Months` / `rentAvgPsf12Months` | Transacted rent per sqft |
| `latestRent` / `highestRent` / `rentMin` / `rentMax` | The rental equivalents |

`priceInsightText` always ships alongside them — the original paragraph, so you
can check any figure that looks surprising.

**These are condo-only.** Sampled across nine projects, the paragraph is present
on every condo and absent on every HDB block and landed estate. A null there is
the normal case for two thirds of Singapore's stock, not a parsing failure.

### Two ways to choose projects

**By ID** — take the `clusterId` column straight from the
**99.co Property Listings Scraper**. The two datasets join on it.

```json
{ "clusterIds": ["debEkK5yYiRSHe6JSjXVB32L", "deqzJz68eCci7cgYgEy8Zr64"] }
```

**By district** — discover every project with an active listing:

```json
{ "districts": ["D9", "D10", "D11"], "propertyType": "condo", "maxItems": 200 }
```

The two combine; explicit IDs are scraped first.

### Input

| Field | Description |
| --- | --- |
| `clusterIds` | Project IDs, or 99.co URLs with an ID appended to the slug. |
| `districts` | Discover every project with an active listing in these districts. |
| `propertyType` | `all`, `condo`, `hdb`, `landed`. Narrows discovery. |
| `maxItems` | How many projects to return. Default 25. |
| `includeNearbyAmenities` | Nearest MRT, bus stop, school, supermarket, clinic, park, bank, ATM — plus the full list. Default on. |
| `amenityRadius` | How far to look, in metres. Default 2,000. |
| `includeNearbyProjects` | Up to 10 neighbouring developments. Default off. |
| `includePhotos` | Every project photo with its caption. Default off. |
| `maxRunSecs` | Time budget. Discovery is capped at half of it. |

### Amenities, with commute cost

Every nearby place carries walking, driving and transit times — and 99.co's own
**taxi fare estimate**, which is unusual and genuinely useful for commute-cost
analysis:

```json
{
  "category": "MRT Stations", "name": "Marine Terrace", "distanceM": 897,
  "walkTime": null, "driveTime": "2 mins", "transitTime": "9 mins",
  "taxiFare": "S$ 4.77"
}
```

A `null` walkTime is information, not a gap: 99.co omits the walking route when
a place is not realistically walkable. The distance is always there.

The eight flat `nearest*` columns (name, distance, walk time) are what most
runs filter on; `nearbyAmenities` holds all ~80 places for the runs that need
them. Both come from one request.

### What this Actor does NOT return

- **Price history as a time series.** The transacted figures are the summary
  statistics 99.co publishes — six-month and twelve-month averages, latest and
  highest — not a transaction-by-transaction ledger.
- **Transacted prices for HDB or landed.** See above; 99.co does not publish
  them in this payload.
- **Projects with no active listing.** 99.co puts a development's ID only
  inside its listings, so a project with nothing currently for sale or rent
  cannot be discovered by district. Supply its ID directly if you have one.
- **IDs for neighbouring projects.** `nearbyProjects` links are slug-only —
  99.co publishes no slug-to-ID lookup, so they cannot be fed back in.
- **Floor plans as images**, unit-level layouts, or developer contact details.

### Notes on the data

- The payload's **key set changes with property type**: condos carry
  `resident_profile` and the price paragraph, HDB and landed carry a
  `description`, landed adds bedroom types and floor-plan counts. Every row
  carries the **same columns** regardless, filled with nulls — so CSV exports
  stay stable.
- `totalUnits` is null when 99.co reports "0 units", which means it does not
  know rather than that the project has no units.
- `ownerOccupiedRatio` / `tenantRatio` are 99.co's own **estimate**, derived
  from rental transaction volume against total units. Condo projects only.
- `marketSegment` (CCR/RCR/OCR) and `districtName` are derived from the
  district number using URA's convention; 99.co does not return them.

### Related

**99.co Property Listings Scraper** — every individual listing with price, PSF,
floor area and agent contacts, and the `clusterId` column that feeds this
Actor. It reaches all 48,000+ listings by working around 99.co's
10,000-result cap.

### Legal

This Actor collects publicly available property information. You are
responsible for using the data in compliance with applicable law and with
99.co's terms.

# Actor input Schema

## `clusterIds` (type: `array`):

99.co project (cluster) IDs — the `clusterId` column produced by the 99.co Property Listings Scraper. A 99.co URL with the ID appended to the slug also works. Leave empty and use Districts instead to discover projects.

## `districts` (type: `array`):

Finds every project with at least one active listing in the chosen postal districts. Projects with nothing currently listed cannot be found — 99.co publishes a development's ID only inside its listings.

## `propertyType` (type: `string`):

Narrows which projects are discovered from the districts above. Ignored when you supply project IDs directly.

## `maxItems` (type: `integer`):

How many project profiles to return. This is what you are billed for, one charge per project.

## `includeNearbyAmenities` (type: `boolean`):

Adds the nearest MRT, bus stop, school, supermarket, clinic, park, bank and ATM with distance and walking time, plus the full list of every nearby place with walk, drive, transit and taxi-fare estimates. Costs one extra request per project.

## `amenityRadius` (type: `integer`):

How far out to look for nearby places.

## `includeNearbyProjects` (type: `boolean`):

Adds up to 10 neighbouring developments with a one-line summary each. Note these links are slug-only — 99.co does not publish their IDs, so they cannot be fed back into this Actor.

## `includePhotos` (type: `boolean`):

Adds every project photo with its caption. The main photo is always included.

## `maxRunSecs` (type: `integer`):

The run stops cleanly and keeps everything collected so far once this is reached. Discovery is capped at half of it, so a run always has time to return rows.

## `proxyConfiguration` (type: `object`):

Not needed. 99.co is reached directly — Cloudflare here is a TLS gate this Actor clears without a proxy. A proxy is used automatically only if a request is genuinely blocked.

## Actor input object example

```json
{
  "clusterIds": [
    "debEkK5yYiRSHe6JSjXVB32L",
    "deqzJz68eCci7cgYgEy8Zr64"
  ],
  "districts": [],
  "propertyType": "all",
  "maxItems": 25,
  "includeNearbyAmenities": true,
  "amenityRadius": 2000,
  "includeNearbyProjects": false,
  "includePhotos": false,
  "maxRunSecs": 210,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `dataset` (type: `string`):

Dataset containing every scraped project.

# 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 = {
    "clusterIds": [
        "debEkK5yYiRSHe6JSjXVB32L",
        "deqzJz68eCci7cgYgEy8Zr64"
    ],
    "propertyType": "all",
    "maxItems": 25,
    "maxRunSecs": 210
};

// Run the Actor and wait for it to finish
const run = await client.actor("incognito_mode/99co-project-profile-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 = {
    "clusterIds": [
        "debEkK5yYiRSHe6JSjXVB32L",
        "deqzJz68eCci7cgYgEy8Zr64",
    ],
    "propertyType": "all",
    "maxItems": 25,
    "maxRunSecs": 210,
}

# Run the Actor and wait for it to finish
run = client.actor("incognito_mode/99co-project-profile-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 '{
  "clusterIds": [
    "debEkK5yYiRSHe6JSjXVB32L",
    "deqzJz68eCci7cgYgEy8Zr64"
  ],
  "propertyType": "all",
  "maxItems": 25,
  "maxRunSecs": 210
}' |
apify call incognito_mode/99co-project-profile-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,incognito_mode/99co-project-profile-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/Psd12DiAwiWlbGOf2/builds/uPPFtCp457caH2VkX/openapi.json
