# Zoopla UK Property Listings Scraper (`superslowsloth/zoopla-listings`) Actor

Zoopla UK property for sale or to rent by town, county or postcode. Newest first or most price-reduced first: price, reductions, date listed, beds, baths, tenure, floor area, coordinates, agent. Built for new-listing and price-drop alerts.

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

## Pricing

from $0.70 / 1,000 listing scrapeds

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?

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

## Zoopla UK Property Listings Scraper

Returns UK property listings from [Zoopla](https://www.zoopla.co.uk), for sale or
to rent, as one flat row per property: the asking price, any price reduction, the
date it was listed, bedrooms and bathrooms, tenure, floor area, coordinates and
the agent.

Built for **new-listing and price-drop alerts**. Run it on a schedule with
`sort = newest_listings` (or `addedWithin = 24_hours`) and you have a feed of what
came on the market since the last run; run it with `sort = most_reduced` and you
have the properties whose price just fell, with the amount, the percentage and
the date of the cut.

Small input, no nested objects, no HTML. Its input mirrors the
Rightmove Property Scraper from the same account, so the same `locations`,
`channel`, price, bedroom, radius and sort settings work on both portals.

### Input

| field | meaning |
|---|---|
| `locations` | One per line: a town, county, outcode or postcode (`Bristol`, `Oxfordshire`, `BS8`, `BS8 1AA`). A part of a larger area uses a slash: `London/Islington`, `Bristol/Clifton`. |
| `channel` | `buy` (default) or `rent` |
| `sort` | `newest_listings` (default), `most_reduced`, `lowest_price`, `highest_price`, or Zoopla's own default |
| `addedWithin` | Only properties first listed in the last 24 hours, 3, 7, 14 or 30 days |
| `minPrice`, `maxPrice` | In pounds; per month when renting. Any whole number |
| `minBedrooms`, `maxBedrooms` | `0` selects studios |
| `radius` | Miles beyond the area: 0.25, 0.5, 1, 3, 5, 10, 15, 20, 30 or 40 |
| `propertyTypes` | `houses`, `flats`, `bungalows` |
| `newHomes` | Sales only: `include` (default), `exclude`, `only` |
| `includeSSTC` | Sales only: include sold-subject-to-contract and under-offer stock |
| `maxItems` | Budget for the whole run, in listings. Default 100 |
| `maxPages` | Pages per area. Default and ceiling 40 |
| `requestIntervalSecs` | Pause between pages. Default 1 |
| `proxyConfiguration` | Residential Apify proxy, on by default; see limits |

Every filter is a value Zoopla was seen to honour. Zoopla quietly ignores a value
it does not know (a radius of 7 miles, a "listed within 2 hours"), so a typo
would return the unfiltered search at full price; this actor offers only the
values that changed the result set and refuses the rest before a request is spent.

### Output

One item per property:

| field | meaning |
|---|---|
| `id` | Zoopla's listing id: the number in the listing URL. What a repeated run joins on |
| `url` | The listing on zoopla.co.uk, absolute |
| `title`, `display_address` | Zoopla's headline (`3 bed end terrace house for sale`) and the address as printed |
| `transaction_type`, `channel` | `buy`/`rent`, and the part of Zoopla it lives in: `for-sale`, `to-rent` or `new-homes` |
| `price`, `price_currency` | Asking price in whole pounds; **per month on a rental** |
| `price_frequency` | `monthly` on a rental, `null` on a sale |
| `price_display`, `price_qualifier` | The price as printed (`£410,000`, `£1,200 pcm`) and `Guide price`, `Offers over`, `Offers in region of`, `From` |
| `rent_per_week_display` | Rentals: Zoopla's weekly equivalent as printed, e.g. `£276.92 pw` |
| `price_reduced` | `true` only when Zoopla gives a reduction figure |
| `price_reduction_amount`, `price_reduction_percent`, `price_reduced_on` | The latest cut: whole pounds, percent of the previous price, ISO date |
| `published_on` | ISO date the listing first went live |
| `listing_flag` | Zoopla's badge: `Just added` or `Back to market` |
| `available_from` | Rentals: `immediately` or an ISO date |
| `bedrooms`, `bathrooms`, `reception_rooms` | Counts. `0` bedrooms is a real answer on a studio |
| `property_type` | Zoopla's slug: `flat`, `semi_detached`, `terraced`, `detached`, `end_terrace`, `town_house`, `bungalow`, `land`... |
| `tenure`, `tags` | `Freehold`/`Leasehold`/`Share of Freehold` when tagged, and every tag (`Chain free`, `New home`, `Auction`, `Shared ownership`, `Retirement`, `Pets allowed`) |
| `floor_area_sqft`, `floor_area_source` | Square feet, and where Zoopla got it - see below |
| `latitude`, `longitude` | Zoopla's coordinates for the property |
| `summary`, `image_url`, `image_count`, `floorplan_count` | Zoopla's truncated blurb and lead photo, and how many photos and floorplans the listing has |
| `agent_name`, `agent_phone`, `agent_url`, `agent_logo_url`, `agent_branch_id` | The marketing agent or developer |
| `featured_type`, `under_offer` | `featured`/`weekly_featured` for a slot an agent paid for; whether the property is under offer |
| `search_location` | The area slug this row came from |

#### Where the fields come from

Each Zoopla results page carries its listings in a schema.org `ItemList`
(28 products per page): id, price, address, coordinates, bedrooms, bathrooms and
floor area. That is the source for those fields. Zoopla's own page also embeds the
listing cards it draws the visible page from, which hold what the `ItemList`
leaves out: the price qualifier, reductions, the date listed, the badge, tags and
the agent. They are joined to the `ItemList` by listing id. A listing whose card is
missing from a page is still delivered, with those fields `null`.

#### Nulls, not zeros

A missing value is `null`, never `0`. Land and parking have no bedroom count, and
a quarter of listings have no floor area. `price_reduced` is `null`, not `false`,
on a row whose card was missing, because `false` would claim a measurement.

#### `floor_area_sqft` is often an estimate

Of 812 listings with a floor area, 461 took it from comparable homes' energy
certificates (`comparables_epc`) and 8 from a survey of comparables
(`comparables_survey`). Only `structured_data` (the agent stated it),
`floorplan_derived` and `text_derived` are the listing's own figure. Filter on
`floor_area_source` before using the number as a fact about the property.

#### A reduction is only a reduction when there is a figure

Zoopla puts a price-history block on listings that were never cut; it says when
the price was first set and nothing else. Those rows have `price_reduced = false`.
`price_reduction_percent` is the cut as a percentage of the price before it
(a £130,000 flat cut by £120,000 reads `48.0`).

### Limits worth knowing before you run it

- **1,000 listings per area, always.** Zoopla serves 25 results per page and 40
  pages, then answers a 404. London reports "50,000" results, which is Zoopla's
  clip rather than a count. The run log lists the areas that stopped at the limit
  (`stopped at Zoopla's page limit`); for more, give several narrower areas, or
  add `addedWithin`, a price band or a radius. A page also carries 3 paid
  "featured" listings on top of its 25, so a full walk returns a few more than the
  area's count.
- **Residential proxy only.** Zoopla sits behind Cloudflare. Measured 2026-10-20 a
  datacenter proxy was answered 403 "Just a moment..." on 5 of 5 requests, while
  residential exits were served on 10 of 11 over 32 minutes. A single address is
  also challenged after enough requests (roughly 80 pages in under an hour drew
  HTTP 429, and the limit cleared within a minute).
  The actor uses a real Chrome TLS fingerprint, treats a challenge as a refusal
  of the address (a 403 or 429, or a 200 holding the challenge page), switches to a
  fresh exit and retries. A page is about 0.5 MB, so residential bandwidth is the
  main cost of a run.
- **A search with no matches returns nothing, not its neighbours.** When Zoopla
  finds no match it shows properties near the area instead. The actor reads the
  match count and does not emit or charge for those.
- **An unknown place is a failure.** Zoopla answers it with its search page and
  HTTP 200; the run log reports `Zoopla does not recognise that place`. Some
  names resolve to nothing in a URL path: `St Albans` and the ambiguous `St Ives`
  both returned "No results found", which the run reports as `Zoopla did not
  accept the search`. Use the outcode (`AL1`) when a name fails.
- **Duplicates across pages and areas are skipped, and not charged.** A featured
  listing can reappear in its own position on a later page; it is delivered once.
- **Zoopla can change its page at any time.** The data is read from the page's
  embedded JSON, which is not a published API.

### Pricing

Pay per event: a small charge when the run starts, plus one charge per listing
delivered. Skipped duplicates and nearby results are not charged, and a run that
returns nothing charges only the start event.

# Actor input Schema

## `locations` (type: `array`):

Where to search, one per line: a town, a county, an outcode or a full postcode, e.g. "Bristol", "Oxfordshire", "BS8", "BS8 1AA". A part of a larger area is written with a slash, e.g. "London/Islington" or "Bristol/Clifton". Zoopla's own spelling applies, and a name it cannot resolve ("St Albans" and the ambiguous "St Ives" were measured failing) is reported as a failure, never answered with another place's listings; use the outcode instead, e.g. "AL1". A property that two areas both return is delivered and charged once.

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

Which part of Zoopla to search. "buy" reads property for sale (new-build lots are included unless you exclude them below), "rent" reads property to rent, priced per calendar month.

## `sort` (type: `string`):

How Zoopla orders the results before they are read. "Newest listed first" is what makes a repeated run a new-listing feed. "Most price-reduced first" is the price-drop feed. Zoopla's own default puts agents' paid listings first and is not useful for monitoring.

## `addedWithin` (type: `string`):

Only properties first listed within this window. Combine "24 hours" with a daily schedule for a feed of what came on the market since the last run.

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

Lowest price to include, in pounds; per calendar month when renting. Any whole number is accepted.

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

Highest price to include, in pounds; per calendar month when renting. Any whole number is accepted.

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

Fewest bedrooms to include. 0 is a real value and selects studios.

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

Most bedrooms to include.

## `radius` (type: `string`):

How far beyond the area to search. Zoopla accepts only these steps and quietly ignores any other value, so only these are offered. A radius makes overlapping areas likely, which is fine: a property matched twice is delivered and charged once.

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

Restrict to certain property types, using Zoopla's names: houses, flats, bungalows. One per line; leave empty for every type, land included. Zoopla's other categories (land, farms, park homes) are not selectable here because Zoopla answers them with an empty page.

## `newHomes` (type: `string`):

Sales only. "include" is Zoopla's default and mixes developers' new-build lots in with resale homes; "exclude" removes them; "only" returns nothing else. Ignored when renting.

## `includeSSTC` (type: `boolean`):

Sales only. Include properties already under offer or sold STC, which Zoopla hides by default (3,786 homes for sale in Bristol become 7,002). This actor always states the choice explicitly, so a change to Zoopla's default cannot quietly change what a saved run returns.

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

Budget for the whole run, across every area, not per area. A property that matches more than one of your searches, or that Zoopla repeats in a paid featured slot, is delivered and charged once.

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

How deep to walk a single area. A page carries 25 results plus 3 featured listings agents paid to insert. Zoopla itself stops at page 40 (about 1,000 listings) however many match, so 40 is the ceiling; narrower areas or filters reach further than a deeper walk.

## `requestIntervalSecs` (type: `integer`):

How long to wait between result pages. Zoopla sits behind Cloudflare, which challenged a single address after roughly 80 page requests in under an hour; a refusal is answered by switching to a fresh address and retrying, and a longer pause trades wall-clock time for fewer retries.

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

Zoopla is behind Cloudflare. Measured 2026-10-20: a datacenter proxy was challenged ("Just a moment...", HTTP 403) on 5 of 5 requests, while residential exits were served 10 of 11 over 32 minutes, so RESIDENTIAL is the default and changing it will most likely produce an empty run.

## Actor input object example

```json
{
  "locations": [
    "Bristol"
  ],
  "channel": "buy",
  "sort": "newest_listings",
  "addedWithin": "",
  "radius": "0",
  "newHomes": "include",
  "includeSSTC": false,
  "maxItems": 100,
  "maxPages": 40,
  "requestIntervalSecs": 1,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `listings` (type: `string`):

No description

# 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 = {
    "locations": [
        "Bristol"
    ],
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("superslowsloth/zoopla-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 = {
    "locations": ["Bristol"],
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("superslowsloth/zoopla-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 '{
  "locations": [
    "Bristol"
  ],
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call superslowsloth/zoopla-listings --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,superslowsloth/zoopla-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/FeitPOABRqBPNCFGa/builds/z7ct94QWgFKezHICZ/openapi.json
