# Property24 Scraper & New-Listing Monitor - South Africa (`neverempty/property24-listings`) Actor

Turn monitoring on and every later run returns only the listings it has not returned before, so you are never charged for the same property twice. Every Property24.com listing as clean JSON: price with its meaning, bedrooms, bathrooms, floor and erf size, suburb. Decoy tiles dropped. Unofficial.

- **URL**: https://apify.com/neverempty/property24-listings.md
- **Developed by:** [NeverEmpty](https://apify.com/neverempty) (community)
- **Categories:** Real estate, Lead generation, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.57 / 1,000 listing returneds

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

## Property24 Scraper - South Africa Homes for Sale & Rent

Pull property listings from **Property24.com** — South Africa's largest property portal — as clean JSON: the price as a number with its meaning, bedrooms, bathrooms, parking, floor and erf size, suburb, city and province, the agency, Property24's badges and the main image, for sale or to rent, for any city or suburb Property24 covers.

*Unofficial. This Actor is not affiliated with, endorsed by, or sponsored by Property24. All product names are trademarks of their respective owners.*

No API key. Reads the same public pages a visitor sees.

***

### Run it tomorrow and you only get what is new

Schedule this Actor with `monitoringMode: true` and each run returns **only the listings it has not returned before**. Not "here is everything again, please de-duplicate it yourself" — the Actor remembers what it already gave you, so a daily feed of a 4,000-listing search costs you the handful of new homes, not 4,000 rows every morning. **You are never charged for the same property twice.**

Listings your filters removed are remembered too, so widening a filter later does not resurrect them as false "new" listings. Listings that did not fit under your limit are **not** remembered, so they are still waiting on the next run instead of being silently lost.

***

### Why this one is different

#### The decoy tiles are dropped

Every Property24 list page carries **one listing tile that visitors never see**: it is pushed thousands of pixels off-screen by a stylesheet rule with a random class name (measured on all four pages fetched on 2026-09-05, one hidden tile each). A scraper that reads the raw HTML returns it as if it were a real listing — and marks itself as a scraper. This Actor reads the stylesheet, finds those rules and drops the hidden tiles before anything is delivered or charged.

#### Both tile layouts are read

Property24 renders two different tiles: a promotional layout (most of page one) that carries the price only as text, and a standard layout (most of page two onward) that carries the price as structured data plus the street address, an excerpt of the description and the agency name. The tile containers carry random, shuffled class names. This Actor reads both layouts by their meaning rather than by position — measured on 65 live tiles, every one came back with an id, heading, URL, type, price text, suburb, city, province and image.

#### The price means what it says

`price` is a number in rand; `priceType` says whether it is a **sale** price, a **monthly-rent**, a **rent-per-sqm** (commercial and industrial space is often let per square metre — 4 of the 42 rentals measured — and that unit is read from the tile, never assumed to be a month), a weekly, daily or yearly rent, or a development's **sale-range** / **rent-range**, which comes back as `priceFrom` / `priceTo` with the number of units. `rentalTerm` carries the unit for every rental. Price filters compare monthly rents and sale prices only; a per-m² rent is never compared with a monthly budget. A listing marked **POA** keeps `price: null` and `priceText: "POA"` — it is never turned into a zero. Medians in the 63 priced listings measured: **R 2,822,500** for sale, **R 40,000** a month to rent.

#### No agent's personal name

Standard tiles carry the listing agent's personal name and photo. This Actor returns the **agency** name only (`agencyName`, when Property24 writes it as text — on 28 of the 65 tiles measured; a tile that shows only a logo has `agencyName: null` and the logo URL), never the person. Phone numbers and e-mail addresses written into the excerpt are removed.

#### Suburb, city and province from the address, not the caption

Each listing's URL carries the suburb, city, province and Property24's area id (`/for-sale/sea-point/cape-town/western-cape/11021/117538814`). Those are returned as `suburb`, `city`, `province` and `areaId` — so a search across a whole city still tells you the suburb of every home, and the area id is exactly what you need to search that suburb next.

#### One property, five agencies — counted, not repeated

When several agencies list the same property, Property24 shows it once with a "grouped listings" link. This Actor returns that listing once and reports `agentsListingSameProperty` (5 in the measured case) so you can tell a sole mandate from a widely listed home.

#### Filters run here

Every filter — price, bedrooms, bathrooms, parking, floor and erf size, property type, suburb, auction, reduced, on show, developments, keywords, agency — runs on the rows already fetched. Only the place is sent to Property24 (plus the newest-first sort in monitoring mode). Nothing extra is requested and nothing extra is charged. A listing is never dropped for a value Property24 did not publish.

***

### Input

Either paste a Property24 listing page URL, or build one from the fields below.

| Field | Type | Default | Description |
|---|---|---|---|
| `searchUrl` | string | — | e.g. `https://www.property24.com/for-sale/cape-town/western-cape/432`. Only the place is kept; sort and price parameters are dropped |
| `channel` | string | `sale` | `sale` (/for-sale) or `rent` (/to-rent) |
| `location` | string | `cape-town` | A city Property24 lists on its home page (`cape-town`, `johannesburg`, `pretoria`, `durban`, `sandton`, `centurion`, `stellenbosch`, `gqeberha` …) or an area path with its id copied from any Property24 address, e.g. `sea-point/cape-town/western-cape/11021`. Property24 answers 404 without the id, so an unknown city is refused before anything is requested |
| `maxListings` | integer | 20 | 1-1000. One listing = one row = one charged event. 20 listings per page; Cape Town for sale runs to 180 pages |
| `maxPages` | integer | 0 | Hard cap on requests; 0 = decide from `maxListings`. Reading pages is free |
| `useProxy` | boolean | `false` | Off by default — Property24 answers Apify's network directly. Switches on by itself if blocked |
| `keywords` / `keywordMatch` / `excludeKeywords` | array / string / array | — | Match on heading, type, suburb, city, province, agency and price text |
| `agencyNames` | array | — | Keep only these agencies (partial match; tiles without a written agency name are kept) |
| `minPrice` / `maxPrice` | integer | 0 | ZAR. Sale price or monthly rent. **A POA listing is kept**, a per-m² / weekly / daily rent is kept (not comparable), and a development is kept whenever any part of its range fits |
| `requirePrice` | boolean | `false` | Drop POA listings |
| `minBedrooms` / `maxBedrooms` | integer | -1 | -1 = off. Property24 lists studios as 0.5 bedrooms. **An unpublished count is kept** |
| `minBathrooms` | number | 0 | Half bathrooms exist (3.5) |
| `minParkingSpaces` | integer | 0 | |
| `minFloorSizeSqm` / `maxFloorSizeSqm` | integer | 0 | m² |
| `minErfSizeSqm` / `maxErfSizeSqm` | integer | 0 | m² (erf = plot) |
| `propertyTypes` | array | — | Partial match on Property24's words: `Apartment`, `House`, `Townhouse`, `Vacant Land`, `Farm`, `Commercial Property`, `Industrial Property` (`house` also matches `Townhouse`) |
| `suburbs` | array | — | Partial match on the suburb from the listing address |
| `excludeDevelopments` | boolean | `false` | Drop development tiles (a block of units with a From/To range) |
| `excludeAuctions` | boolean | `false` | Drop listings with the AUCTION badge |
| `onlyReduced` | boolean | `false` | Keep only listings with the Reduced banner |
| `onlyOnShow` | boolean | `false` | Keep only listings with an On Show banner (the date comes back as `onShowDate`) |
| `monitoringMode` | boolean | `false` | Read newest-first and return only listings not returned on a previous run |
| `resetMonitoringState` | boolean | `false` | One-shot: forget what was already returned |

```json
{ "channel": "sale", "location": "sea-point/cape-town/western-cape/11021", "maxListings": 100,
  "minBedrooms": 2, "maxPrice": 6000000, "excludeAuctions": true, "excludeDevelopments": true }
```

#### Monitoring mode turns this into a daily feed

Property24's default order puts paid placements first, not the newest listings. In monitoring mode this Actor reads the search in Property24's own **newest-first** order (`?sp=so%3dNewest`; on 2026-09-05 only one of the 22 tiles on the default first page was among the 22 newest) and returns **only the listings it has not returned before**, reading on past the ones it already knows — up to 60 pages, or `maxPages` — until it has `maxListings` new ones. Listings removed by your filters are remembered too, so changing a filter later does not resurrect them as false "new" listings — and listings that simply **did not fit under `maxListings`, or were cut off by the run's charge limit, are not remembered**, so they are still waiting for you on the next run rather than being silently lost.

Measured on 2026-09-05: **3,600** homes for sale in Cape Town (180 pages); **7,213** to rent (361 pages).

***

### Output

One row per listing:

```json
{
  "source": "property24.com",
  "status": "ok",
  "listingId": "117560550",
  "title": "0.5 Bedroom Apartment / flat on auction in Gardens - Cape Town",
  "url": "https://www.property24.com/for-sale/gardens/cape-town/western-cape/9145/117560550",
  "searchedLocation": "Cape Town, Western Cape",
  "transactionType": "sale",
  "propertyType": "Apartment",
  "isDevelopment": false,
  "developmentUnits": null,
  "listingTier": "boosted",
  "agentsListingSameProperty": null,
  "description": "Certified and Transparent Non-Distressed Auction Platform. Live Auction 17 September 2026 at 10am. No Buyer Commission. Make an offer to stop the auction or register to bid on ...",
  "price": 1995000,
  "priceCurrency": "ZAR",
  "priceType": "sale",
  "priceFrom": null,
  "priceTo": null,
  "priceText": "R 1 995 000",
  "rentalTerm": null,
  "bedrooms": 0.5,
  "bathrooms": 1,
  "parkingSpaces": null,
  "floorSizeSqm": 26,
  "erfSizeSqm": null,
  "address": "506 72 On Kloof, 5 Nicol",
  "suburb": "Gardens",
  "city": "Cape Town",
  "province": "Western Cape",
  "areaId": "9145",
  "labels": ["AUCTION", "On Show"],
  "isOnShow": true,
  "onShowDate": "06 SEP",
  "isReduced": false,
  "isAuction": true,
  "hasNoTransferDuty": false,
  "agencyName": "Harcourts Atlantic",
  "agencyLogoUrl": "https://images.prop24.com/213885973/Ensure528x153",
  "mainImageUrl": "https://images.prop24.com/385418221/Crop600x400",
  "scrapedAt": "2026-09-05T10:17:08.708Z"
}
```

Measured across 65 live tiles (63 distinct listings — a premium placement can repeat a listing) on 2026-09-05 — 42 homes for sale and 21 to rent in Cape Town plus 2 developments: id, heading, URL, type, price text, suburb, city, province, area id and image on **65 of 65**; a price as a number on **63** (the two developments carry a range instead); bedrooms on **50** and bathrooms on **51** (commercial and industrial listings, and a few flats, have none); parking on **40**; floor size on **41**; erf size on **13**; a street address on **18** and a description excerpt on **23** (standard tiles only); an agency name written as text on **28**; a logo on **57**. A development tile comes back with `isDevelopment: true`, `priceType: "sale-range"` (or `"rent-range"` on a rental search), `priceFrom` / `priceTo` and `developmentUnits`.

#### Rows that are never charged

| `status` | when |
|---|---|
| `no-results` | Property24 reports zero listings for this search. That is its answer, not a failure |
| `no-such-search` | Property24 answered HTTP 404 — what it does for an area path or id it does not know. **Not** a claim that there is nothing there |
| `no-filter-match` | Listings were read, but your filters removed all of them. The row says how many were read |
| `no-new-listings` | Monitoring mode: nothing new since the previous run |
| `unreadable` | The page could not be read, its tiles were missing, or it claimed a total above zero while yielding nothing |

***

### Pricing

Pay per listing returned. Rows that report an empty search, an unknown area, a filter that matched nothing, or a page that could not be read are **not** charged. Hidden decoy tiles are dropped before delivery and never charged.

***

### Notes

- `robots.txt` was read in full on 2026-09-05. It disallows the advanced-search pages, autocomplete, grouped-listing and map handlers, agent zone, login and account pages. This Actor requests only `/for-sale/...` or `/to-rent/...` listing pages with their page number and, in monitoring mode, the newest-first sort.
- Only public pages are read. No login, no API key.

# Actor input Schema

## `searchUrl` (type: `string`):

Paste a Property24 listing page, e.g. https://www.property24.com/for-sale/cape-town/western-cape/432 or https://www.property24.com/apartments-to-rent/sandton/gauteng/109. Only the place (the path with its area id) is used; sort and price parameters are dropped because every filter runs on the rows already fetched. When set, channel and location are ignored.

## `channel` (type: `string`):

sale = /for-sale, rent = /to-rent. A rental's unit (monthly, per m², weekly, daily, yearly) is read from the tile and returned as rentalTerm.

## `location` (type: `string`):

A city Property24 lists on its home page (cape-town, johannesburg, pretoria, durban, sandton, centurion, stellenbosch ...) or an area path with its Property24 id copied from any Property24 address, e.g. sea-point/cape-town/western-cape/11021. Property24 answers 404 without the id, so an unknown city is refused before anything is requested.

## `maxListings` (type: `integer`):

How many listings to return. You are charged for the rows you actually receive. Property24 shows 20 listings per page; a search can run to hundreds of pages (Cape Town for sale: 180 pages).

## `maxPages` (type: `integer`):

Hard cap on the number of list pages requested. 0 = decide from maxListings (about 20 listings per page, plus a margin). Reading pages is free; only returned listings are charged.

## `useProxy` (type: `boolean`):

Off by default: Property24 answers Apify's network directly. If a request is blocked (403/429/503) the Actor switches to a proxy session by itself and retries.

## `keywords` (type: `array`):

Keep only listings whose heading, type, suburb, city, province, agency or price text contains these words. Matching is case-insensitive.

## `keywordMatch` (type: `string`):

any = at least one keyword must appear; all = every keyword must appear.

## `excludeKeywords` (type: `array`):

Drop listings containing any of these words.

## `agencyNames` (type: `array`):

Keep only listings from these agencies (partial match). The agency name is only published as text on boosted, premium and development tiles; a listing whose tile shows only a logo has no name and is kept.

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

Sale price, or monthly rent. 0 = off. A POA listing is kept, a per-m², weekly or daily rent is kept (not comparable with a monthly budget), and a development is kept whenever any part of its range fits.

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

Sale price, or monthly rent. 0 = off. A POA listing and a per-m², weekly or daily rent are kept.

## `requirePrice` (type: `boolean`):

Drop listings whose price is POA (price on application).

## `minBedrooms` (type: `integer`):

-1 = off. Property24 lists studios as 0.5 bedrooms. A listing with no bedroom count (commercial, land) is kept.

## `maxBedrooms` (type: `integer`):

-1 = off.

## `minBathrooms` (type: `number`):

0 = off. Property24 counts half bathrooms (3.5).

## `minParkingSpaces` (type: `integer`):

0 = off.

## `minFloorSizeSqm` (type: `integer`):

0 = off. A listing with no published floor size is kept.

## `maxFloorSizeSqm` (type: `integer`):

0 = off.

## `minErfSizeSqm` (type: `integer`):

0 = off. A listing with no published erf size is kept.

## `maxErfSizeSqm` (type: `integer`):

0 = off.

## `propertyTypes` (type: `array`):

Partial match on Property24's words: Apartment, House, Townhouse, Vacant Land, Farm, Commercial Property, Industrial Property. A listing with no type is kept.

## `suburbs` (type: `array`):

Keep only listings in these suburbs (partial match on the suburb from the listing address, e.g. Sea Point).

## `excludeDevelopments` (type: `boolean`):

Drop development tiles (a block of units with a From/To price range rather than one home).

## `excludeAuctions` (type: `boolean`):

Drop listings carrying Property24's AUCTION badge.

## `onlyReduced` (type: `boolean`):

Keep only listings carrying Property24's Reduced banner.

## `onlyOnShow` (type: `boolean`):

Keep only listings with an On Show banner (the show date is returned as onShowDate).

## `monitoringMode` (type: `boolean`):

Turn this on and schedule the Actor: each run reads the search in Property24's newest-first order and returns only the listings it has never returned before, reading on past the ones it already knows (up to 60 pages, or maxPages) until it has maxListings new ones. Listings removed by your filters are remembered too. Listings that did not fit under the limit are not remembered, so they are still waiting for you on the next run.

## `resetMonitoringState` (type: `boolean`):

One-shot: forget every listing returned so far for this search. The next monitoring run returns everything again.

## Actor input object example

```json
{
  "channel": "sale",
  "location": "cape-town",
  "maxListings": 20,
  "maxPages": 0,
  "useProxy": false,
  "keywords": [],
  "keywordMatch": "any",
  "excludeKeywords": [],
  "agencyNames": [],
  "minPrice": 0,
  "maxPrice": 0,
  "requirePrice": false,
  "minBedrooms": -1,
  "maxBedrooms": -1,
  "minBathrooms": 0,
  "minParkingSpaces": 0,
  "minFloorSizeSqm": 0,
  "maxFloorSizeSqm": 0,
  "minErfSizeSqm": 0,
  "maxErfSizeSqm": 0,
  "propertyTypes": [],
  "suburbs": [],
  "excludeDevelopments": false,
  "excludeAuctions": false,
  "onlyReduced": false,
  "onlyOnShow": false,
  "monitoringMode": false,
  "resetMonitoringState": false
}
```

# Actor output Schema

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

One row per listing: heading, the price as a number with its currency and what it means (sale, monthly rent, rent per m² or per week/day/year as the tile says, or a development's From/To range with the number of units), the price text as Property24 shows it (POA stays null), bedrooms, bathrooms, parking spaces, floor and erf size, street address where Property24 publishes it, suburb, city, province and area id read from the listing address, badges (On Show with its date, Reduced, Auction, No Transfer Duty, Available Now), the listing tier (premium, boosted, standard), how many agencies list the same property, the agency name where it is written as text, the agency logo and the main image. Every row names the place Property24 resolved the search to. Hidden decoy tiles are dropped and agents' personal names are never returned. Searches with no results, unknown area paths (404), filters that matched nothing, and pages that could not be read come back as their own rows and are not charged.

# 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 = {
    "searchUrl": "",
    "keywords": [],
    "excludeKeywords": [],
    "agencyNames": [],
    "propertyTypes": [],
    "suburbs": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("neverempty/property24-listings").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 = {
    "searchUrl": "",
    "keywords": [],
    "excludeKeywords": [],
    "agencyNames": [],
    "propertyTypes": [],
    "suburbs": [],
}

# Run the Actor and wait for it to finish
run = client.actor("neverempty/property24-listings").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 '{
  "searchUrl": "",
  "keywords": [],
  "excludeKeywords": [],
  "agencyNames": [],
  "propertyTypes": [],
  "suburbs": []
}' |
apify call neverempty/property24-listings --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,neverempty/property24-listings"
        }
    }
}

```

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/Ye5CBXAk83pcPgY4k/builds/PeNnasGMbbhHhjome/openapi.json
