# 99.co Singapore Properties Scraper (`scrapyx/99co-properties-scraper`) Actor

Singapore property listings from 99.co (rent + sale): price, price-per-sqft, beds/baths, floor area, built year, furnishing, coordinates and neighbourhood — by listing type and bedroom count.

- **URL**: https://apify.com/scrapyx/99co-properties-scraper.md
- **Developed by:** [Ibnu Adzim](https://apify.com/scrapyx) (community)
- **Categories:** Real estate
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.26 / 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

## 99.co Singapore Properties Scraper

Scrapes property listings from [99.co](https://www.99.co), a major
Singapore property portal, for rent and for sale: price, price-per-sqft,
bedrooms/bathrooms, floor area, build year, furnishing, exact coordinates
and neighbourhood.

***

### What you get

One `PROPERTY` row per listing, plus a `SEARCH_SUMMARY` row per query.

`listing_id` · `url` · `formatted_address` · `main_category` /
`formatted_sub_category` (e.g. "HDB 3 Rooms") · `price` · `psf`
(price per square foot) · `beds` · `bathrooms` · `floorarea_sqft` ·
`top` (build year, e.g. "Built: 2004") · `furnishing` ·
`neighbourhood_name` · `region_name` · `lat` / `lng` ·
`listing_status` · `posted_at_formatted` · `photo_counts` /
`video_count` / `floor_plan_count`

`price`, `psf`, `beds`, `bathrooms` and `floorarea_sqft` are each an
object (`{"value": ..., "unit": ..., "formatted_string": ...}`), passed
through exactly as the site's own data layer shapes them.

***

### Input

```jsonc
{
  "queries": [
    { "listingType": "rent", "bedrooms": "2" }
  ]
}
```

Each entry needs `listingType` (`"rent"` or `"sale"`); `bedrooms` is
optional.

#### `bedrooms` is a validated list (1-5), not free text

Unlike most filters shipped by this portfolio, `bedrooms` is gated to the
5 values 99.co's own site actually links to (`1`, `2`, `3`, `4`, `5`).
That's a deliberate difference from, say, a location filter elsewhere:
those were proven **honest** on an unrecognised value (a clean empty
result or 404); this specific URL segment was only verified for those 5
values, and sending something else risks landing on some other,
unverified page rather than a guaranteed miss — so it's refused up front
instead of guessed at.

***

### Read this before assuming you can page through more results

**There is no working pagination on this site.** `?page=2` (and every
other value tried) returns the exact same listings as `?page=1` — not a
policy restriction, a real check: full id-set comparison, not just a
glance at counts. Each query in `queries` therefore returns **one fixed
batch** (consistently ~36 listings in testing), never more, however many
listings actually exist for that combination. To cover more ground, add
more `queries` entries — e.g. one per bedroom count, or both `rent` and
`sale` — rather than expecting depth from a single query.

***

### Known limits

**1. No total-count field exists anywhere on this target.** The
`SEARCH_SUMMARY` row reports `propertiesReturned` (what actually came
back) and a `fixedBatchNote` explaining the pagination situation above —
there is nothing upstream to report a true total from.

**2. Only Singapore is covered.** 99.co also operates an Indonesia
market (`/id/` paths) with different URL conventions not explored this
session.

**3. Filtering by neighbourhood, price range, property type or sort order
is not offered.** `robots.txt` disallows the query-string parameters
99.co's own filter UI uses for all of these (`?price_min=`, `?bathrooms=`,
`?condo_sub_type=`, `?sort_field=`, and many more) — this actor only uses
the clean PATH-segment forms the site links to itself (`/singapore/rent`,
`/singapore/rent/2-bedrooms`), which cover listing type and bedroom count
only.

***

### Anti-bot posture

**Cloudflare — and the exit IP's geography matters more than you'd
expect.** Local testing needed only pacing: a burst of unpaced recon
requests drew a genuine Cloudflare managed challenge
(`cf-mitigated: challenge`, HTTP 429, "Just a moment...") that cleared on
its own after a cooldown. Real cloud runs were a different story —
Apify's unpinned Residential proxy pool, and even a direct connection
with no proxy at all, both failed **every** retry against this same
challenge for several minutes straight. Pinning the proxy to Singapore
fixed it immediately. **This actor's default proxy configuration is
Residential pinned to `SG`** — Cloudflare here appears to score exit-IP
geography against the market this site serves, the same diagnosis
already on record for this portfolio's Indonesia real-estate actor.
Removing the country pin is likely to make cloud runs fail outright, not
just run slower.

### Policy

`robots.txt` (`User-agent: *`) names no `ClaudeBot`/`anthropic-ai`/
`GPTBot` group, but disallows dozens of query-string parameters covering
essentially all of 99.co's own rich-filter UI (price, beds, baths,
property sub-type, sort, map bounds, keywords, and more), plus the whole
`/singapore/s/*` map-search surface. This actor never touches any
disallowed path — only the clean, site-linked PATH-segment forms
(`/singapore/{rent|sale}`, optionally `/{N}-bedroom(s)`).

# Actor input Schema

## `queries` (type: `array`):

One fetch per entry, each with its own SEARCH\_SUMMARY row. Each item: {"listingType": "rent", "bedrooms": "2"} -- 'listingType' is required ('rent' or 'sale'), 'bedrooms' is optional (1-5). There is no working pagination on this site (confirmed: ?page= returns identical listings every time), so each query returns one fixed batch (observed ~36 listings) -- to cover more ground, add more entries (e.g. one per bedroom count, or both rent and sale).

## `maxConcurrency` (type: `integer`):

How many requests may be in flight at once. This absorbs latency variance; it is NOT the throttle — the crawl rate is set by 'Minimum interval between requests' below.

## `minRequestInterval` (type: `number`):

The actual speed control: the shortest gap between two requests, across all workers. Set higher than most actors in this portfolio by default — a burst of unpaced recon requests triggered a genuine Cloudflare managed challenge during testing (a single paced request never did), so this target is treated as WAF-present, not WAF-free.

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

Residential pinned to Singapore is the cloud default. An unpinned Residential pool — and even a proxy-less direct connection — both failed every retry against a Cloudflare challenge when tested from Apify's cloud, even though local testing never needed a pin at all. Pinning the exit IP to Singapore (the market this actor covers) fixed it immediately — the same diagnosis as this portfolio's Indonesia real-estate actor: some regional sites score exit-IP geography against their own market, not just ASN reputation.

## Actor input object example

```json
{
  "queries": [
    {
      "listingType": "rent",
      "bedrooms": "2"
    }
  ],
  "maxConcurrency": 3,
  "minRequestInterval": 1,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "SG"
  }
}
```

# Actor output Schema

## `items` (type: `string`):

One row per scraped record. See the dataset's default view for field definitions.

# 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 = {
    "queries": [
        {
            "listingType": "rent",
            "bedrooms": "2"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapyx/99co-properties-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 = { "queries": [{
            "listingType": "rent",
            "bedrooms": "2",
        }] }

# Run the Actor and wait for it to finish
run = client.actor("scrapyx/99co-properties-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 '{
  "queries": [
    {
      "listingType": "rent",
      "bedrooms": "2"
    }
  ]
}' |
apify call scrapyx/99co-properties-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scrapyx/99co-properties-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/znnmUaNYknUHxqjS3/builds/0fF9D0uTWUucAGklx/openapi.json
