By the end of this tutorial you will have a Python script that pulls UK property listings from zoopla.co.uk into structured rows: price, address, bedrooms, property type, the agent or developer that listed it, photo counts and map position.
Zoopla is a UK property portal. Zoopla Scraper (reventadata/zoopla-scraper) is the Apify Actor built for it. You send a search, and it returns one row per listing, for sale or to rent.
This guide runs it from Python with the Apify API client. The sample rows below come from a run of the Actor's public example task on 15 November 2026 (London, for sale, 10 rows), not from an invented example.
Prerequisites
You need three things:
- Python 3.11 or newer (the Apify client for Python 3.x requires it).
- A free Apify account, which you can create with an email address or a GitHub login.
- Your API token, from Settings > API & Integrations in Apify Console.
Install the client and export the token:
pip install -U "apify-client~=3.0" export APIFY_API_TOKEN="your_apify_token_here" No scraping code is needed on your side. The Actor does the fetching, and the proxy is handled for you.
What you'll achieve
You will search zoopla.co.uk for homes for sale in London, read the rows in Python and print two lines per listing, including a price per square foot that you work out yourself from two fields.
Some rows will have no floor area, so you will also see how to skip them cleanly. The same script covers a buyer comparing asking prices, an agent watching a patch for new listings, or an analyst building a table of listings by agent. Only the input changes.
Step 1: Open Zoopla Scraper
Open the Zoopla Scraper page on Apify Store. The Try for free button there opens the Actor in Apify Console. This tutorial calls the Actor from Python, so all you need is its name, reventadata/zoopla-scraper.
Step 2: Choose the search
A search is described by a handful of input fields:
-
locations(array, required): Zoopla location slugs such aslondon,manchesterorleeds. Copy the slug from a search URL on zoopla.co.uk, the part after/property/. Each entry is one search, and capital letters or spaces are tidied into the slug form. -
listingType:for-saleorto-rent. Any other value is refused before the run starts. -
minBedrooms: whole number, at least 1. -
minPriceandmaxPrice: whole pounds, at least 1. The asking price when for sale, the monthly rent when to rent. -
sortBy:recommended(Zoopla's default order) orlowestPrice. Anything else is refused before the run starts. -
maxItems: cap on rows across all locations (100 when unset). -
maxPagesPerLocation: cap on result pages per location, between 1 and 40.
The bedroom, price and sort inputs go to Zoopla with the search, so the site does that filtering. A minPrice above maxPrice ends the run as failed with a message before any page is fetched.
The input of the example task whose rows this tutorial uses (listingType is left out, so it takes its default, for-sale):
{ "locations": ["london"], "maxItems": 10 } Step 3: Run it from Python
This script starts the Actor, waits for it, reads the dataset and prints two lines per listing:
import os from apify_client import ApifyClient # 1. Initialise the client with your token from the environment client = ApifyClient(os.environ["APIFY_API_TOKEN"]) # 2. Describe the search: homes for sale in London (the default listing type), at most 10 rows run_input = { "locations": ["london"], "maxItems": 10, } # 3. Start the Actor and wait for it to finish run = client.actor("reventadata/zoopla-scraper").call(run_input=run_input) if run is None or run.status != "SUCCEEDED": status = run.status if run else "unknown" raise SystemExit(f"Run did not succeed (status: {status}).") # 4. Read the rows from the run's default dataset items = client.dataset(run.default_dataset_id).list_items().items print(f"Retrieved {len(items)} listings.\n") # 5. Print two lines per listing, with a price per square foot worked out here for item in items: price, sqft = item.get("price"), item.get("sizeSqft") price_text = f"{price:,} GBP" if price is not None else "no price" per_sqft = f"{price / sqft:.0f} GBP/sq ft" if price and sqft else "no floor area" print(f"{item.get('address')} | {item.get('bedrooms')} bed {item.get('propertyType')} | {price_text} | {per_sqft}") print(f" {item.get('agentName')}, published {item.get('publishedDate')}, tags: {', '.join(item.get('tags') or [])}") Keep maxPagesPerLocation small while you test: a page holds 25 listings, and the more pages you ask for, the longer the run takes.
Step 4: Read the output
The loop prints one block per listing. This is the start of the output for the 10 rows of the 15 November run (the first four listings; I ran the same print loop over the saved rows):
Retrieved 10 listings. "Type 2_05" at Fielders Crescent, Barking IG11 | 2 bed flat | 385,000 GBP | 442 GBP/sq ft Bellway - Fielders Quarter, published 2026-11-13, tags: New home, Chain free Seagull Lane, London E16 | 3 bed flat | 150,625 GBP | 153 GBP/sq ft Riverside - Aqua Gardens Royal Eden Docks, published 2026-11-13, tags: New home, Shared ownership, Leasehold Seagull Lane, London E16 | 3 bed flat | 602,500 GBP | no floor area Riverside - Aqua Gardens Royal Eden Docks, published 2026-11-13, tags: New home, Shared ownership, Leasehold Store Road, London E16 | 1 bed flat | 91,250 GBP | 140 GBP/sq ft Hyde New Homes - Stories Wharf, published 2026-11-13, tags: New home, Shared ownership, Leasehold The third listing has no floor area, so the script prints no floor area instead of dividing. The Actor reports a missing size as null, never as 0.
Understanding your results
Each row carries 41 fields. The ones you will use most:
-
price,priceCurrency,priceLabel: the number,GBP, and the price text exactly as Zoopla shows it (£385,000). -
title,address,propertyType,bedrooms,bathrooms,livingRooms,sizeSqft: what the listing is. A figure the listing does not show isnull. -
publishedDate,publishedLabel,flag,underOffer: when it went up, as ISO date and as Zoopla writes it, plus the badge (Just added). -
agentName,agentPhone,agentId,agentUrl: the estate agent or developer branch, with the public phone number it lists. -
latitude,longitude,imageUrl,imageCount,floorPlanCount,videoCount: position and media counts. -
tags,featureTags,summary,isNewHome,isPremium,displayType: badges, feature tags, the short description, and placement flags. - Rent searches also fill
priceUnit(pcm, orpppmfor shared accommodation),pricePerWeek,availableFromandavailableFromDate; price reductions fillpriceReduction.
One row from the same run, the first of the ten (featureTags cut to its first four entries; everything else as returned):
{ "id": "74769323", "url": "https://www.zoopla.co.uk/new-homes/details/74769323/", "listingType": "for-sale", "title": "2 bed flat for sale", "address": "\"Type 2_05\" at Fielders Crescent, Barking IG11", "propertyType": "flat", "bedrooms": 2, "bathrooms": null, "livingRooms": null, "sizeSqft": 872, "price": 385000, "priceCurrency": "GBP", "priceLabel": "£385,000", "priceUnit": null, "pricePerWeek": null, "priceReduction": null, "publishedDate": "2026-11-13", "publishedLabel": "13th Nov 2026", "flag": "Just added", "underOffer": false, "availableFrom": null, "availableFromDate": null, "isNewHome": true, "tags": [ "New home", "Chain free" ], "featureTags": [ "balcony", "bath", "built_in_wardrobes", "en_suite" ], "summary": "Brand new & chain free two double bedroom apartment featuring four juliet balconies, spacious open-plan kitchen/living/dining area & en suite to ...", "agentName": "Bellway - Fielders Quarter", "agentPhone": "020 3641 2464", "agentId": 115115, "agentUrl": "https://www.zoopla.co.uk/new-homes/developers/branch/bellway-fielders-quarter-barking-115115/", "imageUrl": "https://lid.zoocdn.com/645/430/60f3e9d06658b42ee5dda966bdab181ffe0ffd69.jpg", "imageCount": 36, "floorPlanCount": 1, "videoCount": 0, "latitude": 51.521664, "longitude": 0.118391, "isPremium": true, "displayType": "premium", "location": "london", "searchUrl": "https://www.zoopla.co.uk/for-sale/property/london/", "scrapedAt": "2026-11-15T19:31:09+00:00" } In this run livingRooms, priceReduction, priceUnit, pricePerWeek, availableFrom and availableFromDate were empty on all 10 rows.
Pro tips
- Use
lowestPriceto look at the cheap end.sortBy: "lowestPrice"lists Zoopla's lowest prices first, so a smallmaxItemsshows them without paging far. - Slice big areas. Zoopla serves at most 40 pages of 25 listings, 1,000 per search. For more, split an area into several locations or price bands with
minPriceandmaxPrice. - Read
tagsbefore comparing prices. In this run 9 of the 10 rows carry aShared ownershiptag next to prices from 91,250 to 602,500 GBP. Look attagsandpriceLabelbefore you average anything. - Check repeats at one address. Five of the 10 rows say Pier Road, London E16. A listing that appears twice, on another page or under another location, is stored once, by
id. - Compute your own ratios. There is no price-per-square-foot field. Divide
pricebysizeSqftand skip rows where either isnull. - Rent is monthly. On
to-rentsearchespriceis the monthly figure andpricePerWeekcarries the weekly one.
Troubleshooting
- Zero rows, status SUCCEEDED. The site answered but nothing matched, for example a very low
maxPricewith a highminBedrooms. The status message says so. - Zero rows, status FAILED, "location not found". The slug is one Zoopla does not know. Copy it from a zoopla.co.uk search URL.
- Zero rows, status FAILED, pages could not be read. The status message says the pages could not be read. Start it again later with
maxPagesPerLocationset to 1. - Fewer rows than expected. The log ends with
Skipped N: reasonlines (listings dropped as duplicates) and aPushed N row(s).line. Read it withclient.log(run.id).get().
Zoopla's terms of use restrict automated collection, so check that your use of the data complies with them and with data-protection law. Agent names and phone numbers are returned as listings show them.
Next steps
For current pricing and details, see the Store page.
To repeat a search, save it as an Apify task and attach a schedule, which runs an Actor or task on a cron expression. Datasets export as JSON, CSV, XML, Excel, HTML table, RSS or JSONL (dataset docs), and a webhook can notify your application when a run finishes (webhook docs).
What would you search for on Zoopla first? Drop a comment! 💬
About the author: reventadata builds and publishes data extraction Actors on the Apify Store.

