# 99.co Singapore Scraper (`datascrapers/99-co-scraper`) Actor

Scrape 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.

- **URL**: https://apify.com/datascrapers/99-co-scraper.md
- **Developed by:** [Farhan Ali](https://apify.com/datascrapers) (community)
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.20 / 1,000 dataset items

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.
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?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

**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.

[![99.co Singapore Scraper — input to structured data](https://raw.githubusercontent.com/faizanalii/apify-actor-assets/main/99-co-scraper/99-co-scraper-value-demo.gif)](https://apify.com/datascrapers/99-co-scraper)

### 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.
- `searchQueries` accepts 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.
- `listingType` is sale or rent. It is ignored when the start URL already contains /sale or /rent.
- `propertySegments` accepts 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.
- `rentalType` applies to rent searches: entire unit or room.
- `newLaunch` limits the search to new-launch projects.
- Sort options are relevance, newest, price, price per square foot, and size.
- `maxItems` of 0 does not cap the result count. A positive value stops after that many saved listings.
- `scrapeListingDetails` opens the listing page and, when parsing succeeds, merges description, photos, coordinates, and unit fields onto the same record. That adds a `listing-details` charge.
- `scrapeAgentInfo` opens the agent profile and, when parsing succeeds, adds an `agent` object plus agency fields. That adds an `agent-info` charge.
- 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.

```json
{
  "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:

```json
{
  "searchQueries": ["Orchard"],
  "listingType": "sale",
  "maxItems": 20
}
```

Request with listing-page and agent enrichment:

```json
{
  "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.

1. Start `datascrapers/99-co-scraper` with a JSON input.
2. Wait until the run finishes, or poll the run status.
3. Read items from the run's default dataset.
4. Page the dataset with offset and limit, or export it from the run page.

```python
from apify_client import ApifyClient

client = 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().items
print(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](https://apify.com/datascrapers/propertyfinder-scraper) — property listing records from Property Finder for a different market.
- [Bayut Property Scraper](https://apify.com/datascrapers/bayut-scraper) — Bayut listing records with price and location fields.
- [Dubizzle UAE Scraper](https://apify.com/datascrapers/dubizzle-scraper) — Dubizzle UAE classifieds, including property listings.
- [Rightmove Scraper](https://apify.com/datascrapers/rightmove-scraper) — UK sale and rent listing records for cross-market comparison.
- [Realtor.com Scraper](https://apify.com/datascrapers/realtor-scraper) — US listing records with price and property attributes.
- [Idealista Scraper](https://apify.com/datascrapers/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.

# Actor input Schema

## `startUrls` (type: `array`):

99.co search, listing, or agent URLs copied from the browser (preferred). Examples: "https://www.99.co/singapore/sale", "https://www.99.co/singapore/rent", "https://www.99.co/singapore/sale/property/dover-parkview-condo-duLTxSUTps9PBGEJEBDWFu". Search URLs are paginated as-is; listing URLs are scraped directly; agent URLs scrape that agent's listings. Filters below apply to Search Queries, not to Start URLs.

## `searchQueries` (type: `array`):

Locations or keywords (e.g. "Orchard", "Tampines", "District 9", "Punggol MRT"). Mapped to 99.co's name / query_name search. Leave empty to scrape all listings matching the filters below, or rely on Start URLs.

## `listingType` (type: `string`):

Buy (sale) or rent. Ignored when Start URLs already include /sale or /rent.

## `propertySegments` (type: `string`):

99.co property segment. Residential is the default marketplace.

## `mainCategory` (type: `array`):

High-level type. Leave empty for all (or infer from Property Types below).

## `propertySubTypes` (type: `array`):

Specific types. Leave empty to include all types (or all types in the selected groups). Landed types are sent as landed_sub_type; others as sub_category.

## `minPrice` (type: `integer`):

Minimum asking price in Singapore dollars. For sale this is the total price (e.g. 500000); for rent this is monthly rent (e.g. 2500).

## `maxPrice` (type: `integer`):

Maximum asking price in Singapore dollars.

## `bedrooms` (type: `array`):

Bedroom count. Studio is 0; 5 means 5 or more. Leave empty for any.

## `bathrooms` (type: `array`):

Bathroom count. 5 means 5 or more. Leave empty for any.

## `minSize` (type: `integer`):

Minimum internal floor area in square feet (e.g. 500, 1000).

## `maxSize` (type: `integer`):

Maximum internal floor area in square feet.

## `leaseType` (type: `array`):

Land tenure. Leave empty for any.

## `furnishing` (type: `array`):

Furnishing level. Leave empty for any.

## `rentalType` (type: `string`):

For rent searches only: entire unit vs room rental. Leave empty for all.

## `newLaunch` (type: `boolean`):

Only new launch projects (composite_property_status=new_launch).

## `sortBy` (type: `string`):

Result ordering on search pages.

## `scrapeListingDetails` (type: `boolean`):

Open listing pages in parallel (see Max Concurrency) for description, photos, coordinates, and unit details. Charges the listing-details event per listing in addition to dataset-item.

## `scrapeAgentInfo` (type: `boolean`):

Open the listing agent profile for CEA license, agency, bio, and transaction history. Charges the agent-info event per listing that includes agent data.

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

Maximum number of listings to scrape (0 = unlimited)

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

How many listing-detail (and agent) pages to open at the same time. Each tab uses the same sticky residential proxy. Higher is faster; 3–5 is a good default (e.g. 5).

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

Proxy settings. Apify Residential is required — 99.co sits behind Cloudflare. Use Singapore (SG) IPs.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://www.99.co/singapore/sale"
    }
  ],
  "searchQueries": [
    "Orchard"
  ],
  "listingType": "sale",
  "propertySegments": "residential",
  "rentalType": "",
  "newLaunch": false,
  "sortBy": "relevance",
  "scrapeListingDetails": false,
  "scrapeAgentInfo": false,
  "maxItems": 10,
  "maxConcurrency": 5,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "SG"
  }
}
```

# Actor output Schema

## `results` (type: `string`):

JSON array of scraped 99.co Singapore listings at {{links.apiDefaultDatasetUrl}}/items

## `runStats` (type: `string`):

Record counts and timestamps for this run

## `run` (type: `string`):

Public console page for this run

# 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 = {
    "startUrls": [
        {
            "url": "https://www.99.co/singapore/sale"
        }
    ],
    "searchQueries": [
        "Orchard"
    ],
    "maxItems": 10,
    "maxConcurrency": 5,
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "SG"
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("datascrapers/99-co-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 = {
    "startUrls": [{ "url": "https://www.99.co/singapore/sale" }],
    "searchQueries": ["Orchard"],
    "maxItems": 10,
    "maxConcurrency": 5,
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "SG",
    },
}

# Run the Actor and wait for it to finish
run = client.actor("datascrapers/99-co-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 '{
  "startUrls": [
    {
      "url": "https://www.99.co/singapore/sale"
    }
  ],
  "searchQueries": [
    "Orchard"
  ],
  "maxItems": 10,
  "maxConcurrency": 5,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "SG"
  }
}' |
apify call datascrapers/99-co-scraper --silent --output-dataset

```

## MCP server setup

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