Zoopla Scraper - Property Listings, Sold Prices & Monitoring
Pricing
from $1.00 / 1,000 property results
Zoopla Scraper - Property Listings, Sold Prices & Monitoring
Scrape UK property from Zoopla: for sale, to rent and Land Registry sold prices. Search by place with filters or paste links. Optional full detail adds EPC, floor plans, tenure, price history, stations and agent phone. Incremental mode for change monitoring.
Pricing
from $1.00 / 1,000 property results
Rating
0.0
(0)
Developer
Abot API
Maintained by CommunityActor stats
1
Bookmarked
1
Total users
0
Monthly active users
a day ago
Last modified
Categories
Features
Share
Zoopla UK Property Scraper: For Sale, To Rent, New Homes & Sold Price Data
Zoopla UK Property Scraper turns Zoopla into a structured UK property data feed. Pull live listings for sale, live listings to rent, new-build developments, Land Registry sold-price records and estate-agent branch details, searched by place name with Zoopla's own filters or scraped directly from links you paste. Turn on full listing detail to add EPC rating, floor plans, tenure, council tax band, service charge, UPRN, price history and the agent's phone number, then export to JSON, CSV or Excel, or pull results straight into your app through the API.
Why This Scraper?
- Four channels, one actor. For sale, to rent, new homes and Land Registry sold prices: the same UK place search covers all four, and pasted links also cover estate-agent branch pages.
- Search by place or paste links. Look up a city, postcode or district with Zoopla's own filters, or scrape the exact results page, house-prices page or listing page you already have.
- Full listing detail on demand. Switch on EPC rating and graph images, floor plans, tenure, council tax band, service charge, ground rent, UPRN, price history, nearest stations, EV charge points, virtual tours, brochures and the agent's phone number, billed only per listing you actually enrich.
- UK-specific fields throughout. Postcode, outcode, UPRN, council tax band and EPC rating are first-class fields, and every price is in GBP.
- Built for monitoring. Incremental mode returns only new, updated and reappeared listings on a recurring search, so a daily run does not rebill what has not changed.
- Resume without repaying. Continue an interrupted crawl from its run id and skip everything it already collected.
- A refused run fails loudly. If Zoopla refuses every request, the run stops with a clear message instead of returning an empty dataset that looks like "no results".
Use Cases
- Property investment and comparables: compare current asking prices against Land Registry sold prices for the same postcode to judge whether a listing is fairly priced.
- PropTech and market research: build historical datasets of prices, EPC ratings, tenure and property types across areas for analysis or machine learning.
- Listing and price-drop monitoring: run incremental mode daily to catch newly listed, reduced, let-agreed or sold properties in a target area.
- Agent and branch tracking: paste an estate-agent branch page (or an agents-by-area list) to get the branch's name, address, phone and live listing counts, and track how a listing's price changes over time.
- Relocation and house-hunting tools: feed a personal search (place, budget, bedrooms, property type) into a spreadsheet, bot or app.
Data You Get
Sample shape: values are illustrative placeholders, not from a live record.
| Field | Example |
|---|---|
listingId | "10000001" |
listingType / channel | "sale" / "for-sale" (also "rent" / "to-rent", "sale" / "new-homes", "sold" / "sold", "agent" / "agents") |
url | Zoopla link to the listing or sold record |
title / address | "2 bed flat for sale" / "Example House, Sample Street, Manchester M1" |
price / priceLabel / currency | 285000 / "ยฃ285,000" / "GBP" |
bedrooms / bathrooms / receptionRooms | 2 / 1 / 1 |
propertyType / sizeSqft | "flat" / 720 |
tenure | "leasehold" |
epcRating / councilTaxBand | "D" / "B" (full detail only) |
serviceCharge / groundRent | "ยฃ1,200 per year" / "ยฃ100 per year" (full detail only) |
chainFree | true |
uprn | "0000000000" |
postcode / latitude / longitude | "M1 0AA" / 53.48 / -2.24 |
nearestStations | [{ "name": "Example Street", "miles": 0.4 }] (full detail only) |
features | ["Private balcony", "Allocated parking", "Open-plan kitchen"] (full detail only) |
images / floorPlanImages / epcImages | listing photo, floor plan and EPC graph URLs (full detail only) |
branchName / branchPhone | "Example Estates" / "01000 000000" |
lastSoldPrice / lastSoldDate | 210000 / "2018-03-22" (sold channel) |
publishedOn | "2026-09-12T08:30:00" |
detailScraped | true |
changeType | "NEW" (also "UPDATED", "UNCHANGED", "REAPPEARED", "EXPIRED"; incremental mode only) |
description / thumbnail | card summary text / main photo URL |
featured / roomsCount | false / [{ "type": "bedrooms", "count": 2 }] |
scrapedAt | "2026-09-30T11:00:00Z" |
Sold records carry uprn, address, postcode, latitude, longitude, lastSoldPrice, lastSoldDate, bedrooms, bathrooms, receptionRooms, propertyType, tenure and floorAreaSqM; they have no listing page, so full detail adds nothing to them. Full detail also adds commonholdDetails, materialInformation, broadband, parkingInfo, accessibility, evChargingStations, virtualTours, brochures, priceHistory and smartTags to sale and rent records.
Agent branch records carry branchId, branchName, address, postcode, branchPhone, branchUrl, latitude, longitude, branchSaleListings, branchRentListings, branchAverageSalePrice, branchAverageRent, branchSaleWeeksOnMarket, branchRentWeeksOnMarket, branchSellsProperty and branchLetsProperty. The dataset has five views: Properties (overview), Property details (EPC, tenure, charges, stations, price history, features, floor plans), Sold prices, Estate agents and Changes (incremental changeType and changedFields). description, thumbnail, featured and roomsCount are additive convenience fields; they never mark a row as changed in incremental mode.
How to Use
- Pick a mode:
search(by place name, with filters) orurl(paste links you already have). - In search mode, name your places to search and the channels (for sale, to rent, new homes, sold prices), then set price, bedroom, property type, must-have features and other filters.
- Turn on Fetch full listing details if you need EPC, tenure, price history and the rest, and set Max results to control run size and cost.
- Click Start, then download the dataset as JSON, CSV or Excel, or read it through the API.
Search by place, with filters:
{"mode": "search","locations": ["London", "Manchester"],"channels": ["for-sale"],"minPrice": 200000,"maxPrice": 500000,"minBeds": 2,"propertySubTypes": ["flats"],"includeDetails": true,"maxItems": 50}
Places can be a city (Manchester), a postcode area (SW19), a district or borough on its own (Islington), or a district written the way you would say it (Islington, London).
Paste links:
{"mode": "url","startUrls": ["https://www.zoopla.co.uk/for-sale/property/london/islington/","https://www.zoopla.co.uk/house-prices/london/islington/"],"includeDetails": true}
A results page returns its listings, a house-prices page returns sold records, and a single listing link returns that one property with full detail, whether or not Fetch full listing details is on, so a pasted listing link that is read successfully always carries the full-detail charge. A page number in a pasted address is used as the starting page. Supported shapes: for-sale, to-rent and new-homes results pages (including postcode districts, filters and sort), house-prices pages, for-sale, to-rent and new-homes listing pages, and estate-agent pages (one branch page, or an agents-by-area list). A new-homes results page or search is labelled with the new-homes channel, an agent page returns one agent record per branch, and agent records have no listing page, so full detail adds nothing to them.
If Zoopla links is left unedited, URL mode scrapes the built-in examples, one per shape: a results page, a filtered results page, a to-rent page, a postcode district, a new-homes page, a house-prices page, a for-sale listing, a new-homes listing, a to-rent listing and an estate-agent branch. Listing links are read in full detail, so use Max results to bound the run.
Sold prices only, last 12 months:
{"mode": "search","locations": ["Islington, London"],"channels": ["sold"],"soldPeriodMonths": "12","maxItems": 100}
Daily rental monitoring with incremental mode:
{"mode": "search","locations": ["Manchester"],"channels": ["to-rent"],"incrementalMode": true,"emitExpired": true,"maxItems": 200}
Run it from your code
Python:
from apify_client import ApifyClientclient = ApifyClient("<YOUR_APIFY_TOKEN>")run = client.actor("abotapi/zoopla-co-uk-scraper").call(run_input={"mode": "search", "locations": ["London"], "channels": ["for-sale"], "maxItems": 50})for record in client.dataset(run["defaultDatasetId"]).iterate_items():print(record["address"], record["priceLabel"])
JavaScript:
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });const run = await client.actor('abotapi/zoopla-co-uk-scraper').call({mode: 'search', locations: ['London'], channels: ['for-sale'], maxItems: 50,});const { items } = await client.dataset(run.defaultDatasetId).listItems();
Or connect it to Make, Zapier, n8n, Google Sheets or webhooks from the Integrations tab.
How maxPages and maxItems shape a run
Zoopla serves at most 40 result pages per search, about 1000 records. To go deeper, split the search: narrow by price band, bedroom count, property type, or search smaller places. Premium listings are pinned to the top of the first page whatever sort order you pick, which is Zoopla's own behaviour, not the actor's.
In incremental mode, treat Max results as a cap on how many listings are scanned, not a guarantee of how many new or changed rows you get back. The actor stops turning pages once it has read Max results listings, and only then drops the ones that turn out unchanged. So if the first listings Zoopla shows are ones you already have, they can use up the whole cap: the run can return zero rows and report "nothing changed since the last run" while genuinely new listings sit further down the same page or on later pages. A new listing that was read past the cap this way is also skipped if it shows up again under a later place in the same run. Because the same unchanged listings come first again next time, this repeats on every scheduled run until you change the setup. For monitoring, set Max results well above the number of listings the search shows (or 0 for no cap), or sort by newest listings so new ones are read first.
Resume and recurring updates
- Resume (
resumeFromRunId) continues one interrupted run: paste its run or dataset ID and the actor skips every listing already collected there, so you don't pay twice. It cannot be combined with incremental mode once that search already has saved state; use it to bootstrap a brand-new monitoring campaign instead. If you do bootstrap that way, the listings skipped from the earlier run are tracked by id only, so the first normal incremental run that sees each of them again returns, and bills, it once asUPDATEDwith every field listed inchangedFields, even if nothing changed on Zoopla. From the run after that they behave normally. A skipped listing that disappears before it is seen again is not reported asEXPIRED. - Incremental mode (
incrementalMode) is for running the same search again and again, for example daily. The first run returns everything asNEW. Later runs normally return onlyNEW,UPDATED(withchangedFields) andREAPPEAREDrecords.UNCHANGEDis suppressed and not billed unlessemitUnchangedis on (then it is billed like any returned row, including the Full listing detail event when the listing is enriched).EXPIREDis only produced after a run that scanned the whole tracked search, and only withemitExpiredon.stateKeynames or shares the stored state.ignoreFieldsForChangeslets you exclude further noisy fields from change detection; the comparison is made against the stored previous record using the current list, so changing the list never produces a one-off wave ofUPDATEDrows. One-time exception: a search that sets Min bedrooms or Max bedrooms to0(studio-and-up, or studios only) now has its own automatic state, because that setting really narrows the results. Such a campaign starts a fresh baseline on its next run, so every listing comes back once asNEWand is billed like any other new row. SetstateKeyto keep the name stable. Campaigns that do not set a0bedroom bound, and every other filter at0, keep their saved state. Listings of the new homes channel that an earlier version labelled for sale are matched to their saved record, so the newnew-homeslabel does not mark themUPDATED.
Note on emitExpired and Max results: the EXPIRED batch is computed, pushed and billed as one unit once a run has fully scanned its tracked search, and it is not trimmed against Max results. If a search has accumulated a large tracked baseline and many of those listings have genuinely disappeared since the last run, a capped run can still return, and bill, more EXPIRED rows than Max results allows. Turn emitExpired off, or budget for it separately, on searches with a large tracked history.
Send results into your apps (MCP connectors)
Optionally pipe the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. This is an extra delivery step after the scrape: the Apify dataset is never changed.
What gets written to the connector: a condensed, human-readable summary of each record, not the full JSON. Each item becomes one entry with a title and its key fields flattened to plain text. The complete record always stays in the Apify dataset.
- Authorize a connector once under Apify โ Settings โ Integrations (Notion, Linear, Airtable, or Apify).
- Select it in the "Pipe results into your apps" input field. (If the picker is empty, you haven't authorized a connector yet.)
- For Notion, also set
notionParentPageUrlto the page where items should be created.
The connection is mediated by Apify's MCP proxy, so this actor never sees your third-party credentials. Leave the field empty to skip.
Input Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
mode | string | search | search to look up places by name and apply filters, or url to scrape Zoopla links you paste. |
locations | array | ["London"] | One or more UK places, for example London, Manchester, SW19, or Islington, London. Search mode only. |
channels | array | ["for-sale"] | Any of for-sale, to-rent, new-homes, sold. Search mode only. |
minPrice | integer | (none) | Lowest price to include, in GBP. |
maxPrice | integer | (none) | Highest price to include, in GBP. |
minBeds | integer | (none) | Only properties with at least this many bedrooms. 0 is a real value: studios stay in, listings that state no bedroom count drop out. |
maxBeds | integer | (none) | Only properties with at most this many bedrooms. 0 returns studios only. |
minBaths | integer | (none) | Only properties with at least this many bathrooms. |
propertySubTypes | array | (none, every type) | Detached, semi-detached, terraced, flats, bungalows, farms and land, park homes, offices, retail, hospitality, industrial. On the sold channel only the first type picked is used. |
radius | string | 0 | Widen the search around the place, from This area only up to 40 miles. |
mustHaveFeatures | array | (none) | Only listings that have all the features picked: garden, parking or garage, balcony or terrace. For sale, to rent and new homes. |
addedSince | string | (any time) | Last 24 hours, 3 days, 7 days, 14 days or 30 days. |
keywords | string | (none) | Free-text keyword, for example garden, garage or balcony. |
sortBy | string | recommended | Recommended, newest, highest price, lowest price, most reduced. Premium listings stay pinned to the top regardless. |
chainFreeOnly | boolean | false | For sale only. Only chain-free properties. |
auctionOnly | boolean | false | For sale only. Only auction properties. |
sharedOwnershipOnly | boolean | false | For sale only. Only shared-ownership properties. |
retirementOnly | boolean | false | For sale only. Only retirement properties. |
reducedPriceOnly | boolean | false | Only listings whose asking price has been reduced. |
includeUnderOffer | boolean | false | For sale only. Also return properties under offer or sold subject to contract, hidden by Zoopla by default. |
includeLetAgreed | boolean | false | To rent only. Also return properties let or let agreed, hidden by Zoopla by default. |
furnishedState | string | (any) | To rent only. Furnished, part furnished, or unfurnished. |
availableFrom | string | (any time) | To rent only. Available now, or within 1 month, 3 months, 6 months or 1 year. |
priceFrequency | string | (Zoopla default, per month) | To rent only. Whether minPrice/maxPrice are per month or per week. |
petsAllowed | boolean | false | To rent only. Only rentals that accept pets. |
billsIncluded | boolean | false | To rent only. Only rentals with bills included. |
soldPeriodMonths | string | all-time | Sold prices only. How far back to look for sold records. |
startUrls | array | one example per shape | Zoopla results pages, house-prices pages, listing pages (for sale, to rent, new homes), estate-agent pages, or bare listing ids. URL mode only; filters above are not applied here. |
includeDetails | boolean | false | Add the full detail set to every sale/rent record: EPC, floor plans, tenure, council tax band, service charge, ground rent, UPRN, price history, description, features, nearest stations, EV points, virtual tours, brochures and the agent's phone number. Billed as a per-listing surcharge; no effect on the sold channel. |
maxItems | integer | 20 | Maximum records across the whole run. 0 means unlimited. |
maxPages | integer | (none, every page) | Maximum result pages per place and channel. Empty or 0 covers every page Zoopla offers, at most 40 per search. |
resumeFromRunId | string | (none) | Continue one interrupted run without returning or paying for rows it already collected. |
incrementalMode | boolean | false | Turn on for daily or recurring monitoring; later runs return only NEW, UPDATED and REAPPEARED records. |
stateKey | string | (none) | Name a monitoring campaign, or deliberately share state between runs. |
emitUnchanged | boolean | false | Also return, and bill, UNCHANGED records. |
emitExpired | boolean | false | Also return, and bill, EXPIRED records, only after a run that fully scanned the tracked search. |
ignoreFieldsForChanges | array | [] | Incremental mode only: extra field names (or dotted paths such as priceHistory.date) that must not mark a row UPDATED. They stay in the output but are left out of changedFields. Scrape time, source URL, search mode, the status badge and paid-placement flags are always ignored. |
proxy | object | Apify Proxy | Connection settings. A proxy connection is required for reliable results. |
mcpConnectors | array | (none) | Optional: send a summary of each record to apps you authorized under Integrations. |
notionParentPageUrl | string | (none) | Notion connector only: page under which item pages are created. |
maxNotifyListings | integer | 50 | Cap on items written to each connector per run. Does not affect the dataset. |
Output Example
Sample shape: values are illustrative placeholders, not from a live record.
{"listingId": "10000001","listingType": "sale","channel": "for-sale","url": "https://www.zoopla.co.uk/for-sale/details/10000001/","title": "2 bed flat for sale","address": "Example House, Sample Street, Manchester M1","price": 285000,"priceLabel": "ยฃ285,000","currency": "GBP","bedrooms": 2,"bathrooms": 1,"receptionRooms": 1,"propertyType": "flat","sizeSqft": 720,"tenure": "leasehold","epcRating": "D","epcImages": ["https://lid.zoocdn.com/1024/768/example-epc.png"],"floorPlanImages": ["https://lid.zoocdn.com/1024/768/example-floorplan.jpg"],"councilTaxBand": "B","serviceCharge": "ยฃ1,200 per year","groundRent": "ยฃ100 per year","chainFree": true,"uprn": "0000000000","postcode": "M1 0AA","latitude": 53.48,"longitude": -2.24,"nearestStations": [{ "name": "Example Street", "miles": 0.4 }],"features": ["Private balcony", "Allocated parking", "Open-plan kitchen"],"priceHistory": [],"images": ["https://lid.zoocdn.com/1024/768/example-photo.png"],"branchName": "Example Estates","branchPhone": "01000 000000","branchUrl": "https://www.zoopla.co.uk/find-agents/branch/example-estates-1/","publishedOn": "2026-09-12T08:30:00","detailScraped": true,"scrapedAt": "2026-09-30T11:00:00Z"}
Plan Requirement
The default proxy setting works out of the box, and the actor never upgrades a run to a metered proxy pool on its own. For large or frequent runs, or if a run reports refused requests, switch to the residential proxy group under Connection.
If the site refuses the primary connection for a stretch (it does so in waves), the actor retries over a backup connection on its own, and says so in the run log ("Served over the backup connection (the default connection was unavailable)."). If you chose the residential proxy group and your Apify plan does not include it, the run says so once in the log and carries on over the backup connection; if every connection is refused the run ends as failed with a clear message instead of returning an empty dataset. Runs that bring their own proxy keep the connection they brought. Set the ZOOPLA_ALTERNATE_CONNECTION environment variable to 0 to switch the backup off.
FAQ
How much does it cost?
You pay per property record returned, with full listing detail billed only for listings actually enriched: when you switch it on, and for every listing link you paste in URL mode (those are always read in full). The Pricing tab shows the current rates. Three events are charged. The Actor start event is charged once per run. The Property result event is charged for each record returned in the dataset (listing, sold record or agent branch). The Full listing detail event (detail-enrichment) is charged per listing enriched and returned, right after each page's rows are delivered, so an interrupted run is billed only for what it delivered; listings that are suppressed (including UNCHANGED listings while Emit unchanged records is off) or whose detail page could not be read are not charged it, and agent and sold rows never are. An UNCHANGED listing you choose to receive with emitUnchanged on is billed like any other returned enriched listing: the Property result event plus the Full listing detail event. Use Max results to cap the cost of any run.
Is it legal to scrape Zoopla?
This actor collects only publicly available property data. You are responsible for how you use it: follow Zoopla's terms and the laws that apply to you, including UK GDPR where records carry an agent's contact details, and get legal advice if you plan commercial redistribution.
Can I get only new or changed listings on a schedule?
Yes. Schedule the actor from the Schedules tab and turn on Incremental mode. Each run then returns only new, updated and reappeared records, and unchanged ones are not billed unless you turn emitUnchanged on.
Why did an incremental run come back with fewer rows than Max results?
Max results caps how many listings are scanned, not how many survive incremental filtering. Listings that turn out unchanged still use up the cap, and once it is used up no further listings or pages are read, so a run can even return zero rows while new listings exist further down the results, and the same thing happens again on the next run. Set Max results well above the number of listings your search shows (or 0), or sort by newest listings, for monitoring runs.
Why did every field show as changed on my first run after using Resume with incremental mode?
When a new incremental campaign is started with resumeFromRunId, the listings from that earlier run are recorded by id only, without their field values. On the next normal run each of them is returned once as UPDATED with every field in changedFields, and billed like any other row, even when nothing changed on Zoopla. It happens once per listing; later runs compare real values. To avoid it, start the campaign with a plain incremental run instead of a resume.
Why can an EXPIRED batch push more rows than Max results?
Once a run fully scans its tracked search, emitExpired computes and bills every listing that disappeared since the last run in one batch, without trimming it to Max results. On a search with a large tracked history, this can add more billed rows than Max results implies. Turn emitExpired off on very large monitoring campaigns if this matters to you.
Why did my run fail instead of returning an empty dataset?
If Zoopla refuses every request, the run stops with a clear message so "no listings match" is never confused with "nothing could be read". Try again, or switch the Connection section to a different proxy pool. If only some places or pages could be read, you still get what was found.
Can I use it with AI agents or MCP?
Yes. Call it from any Apify integration or MCP client, and use the connector field to push results into Notion, Linear or Airtable.
๐ More scrapers you might like
Pair this actor with these related scrapers from the same team:
| ๐ Rightmove Fast, reliable Rightmove.co.uk scraper for sale, rent, and sold-price listings. Search by... | ๐ Jitty Scraper Scrape residential property listings from Jitty.com across the UK. Search with 20+... |
| ๐ OnTheMarket Scraper Scrape onthemarket.com for-sale, to-rent and new-homes listings with 85+ fields: price... | ๐ Next.co.uk Scraper Scrape Next.co.uk: keyword search, category and product-listing walks, full product... |
| ๐ผ NHS UK Scrape nhs.uk Jobs listings into a flat dataset. Extract 50+ fields, including pay band... | ๐ Currys UK Scraper Scrape Currys UK products, including electronics, home appliances, computers, TVs and... |
๐ Browse all abotapi scrapers
๐ฌ Support
- ๐ Found a bug or a missing field? Open a ticket on the Issues tab. We usually reply within hours.
- ๐ ๏ธ Need another site, extra fields or a private build? Email contact@abotapi.com.
- โญ Enjoying it? A quick review on the actor page helps other users find it.