Zoopla UK Property API
UK property listings with a dated price history on every row, so a search returns not only the asking price but what the property last sold for and in what year. In live runs the sale events went back to 1998. Ordinary housing stock also carries its UPRN, the government identifier that joins these records to Land Registry and council datasets without fuzzy address matching. Searches start from a place name, a borough or a postcode area rather than a listing URL, which matters because UK listings are pulled within weeks of a sale and a saved URL is usually already dead.
HISTORY FROM1998
MEDIAN LAGon demand
FIELDS37
Input parameters
| PARAMETER | TYPE | REQ | DEFAULT | DESCRIPTION |
|---|---|---|---|---|
| mode | enum | yes | search | search finds live listings from a place name and needs no URL. url collects specific listing pages you already hold. Search is the mode almost everyone wants, because UK listings do not stay up long enough for a saved URL to keep working. |
| locations | string[] | no | — | UK towns, cities, boroughs or postcode areas, for example London, Manchester or SW1. Required in search mode, ignored in url mode, and prefilled with London when you open the Actor. Up to 20 per run. |
| propertyType | enum | no | For sale | Whether to search sale or rental stock. The two accepted values are written differently from each other, and neither is normalised for you. |
| maxResultsPerSearch | integer | no | 50 | Listings returned for each location, from 1 to 2000. You are billed per listing returned, so treat this as the budget ceiling rather than a target. — drives your bill |
| listingUrls | string[] | no | — | Specific listing URLs to collect. Required when mode is url, ignored otherwise, up to 500 per run. A listing that has already been sold or let comes back as an error row and is not charged. |
Output schema
| FIELD | TYPE | DESCRIPTION | NULLABLE |
|---|---|---|---|
| result_type | string | listing or error. Every row is one or the other, so a search that found nothing is visible in the data instead of silently absent. | no |
| searchLabel | string | The location and listing type this row came from, for example "Rochdale (For sale)". Search mode only. | yes |
| propertyId | string | Declared identifier for the listing. It did not come through on any row in live sampling, so treat listingUrl as the practical key; the numeric id sits in its path. | yes |
| listingUrl | string | Direct link to the listing, and in practice the field to diff two runs on. | no |
| propertyTitle | string | The headline as published, which bundles the address, the dwelling type and the price into one string. | yes |
| propertyType | string | Dwelling type as the listing words it, for example "2 bed terraced house for sale". It is descriptive text, not a controlled vocabulary. | yes |
| address | string | Address line as published, usually street plus locality plus the outward postcode rather than a full postcode. | yes |
| price | string | Asking price or rent exactly as displayed, including any qualifier such as offers over or guide price. | yes |
| priceValue | number | The numeric part of the price, parsed for sorting and filtering. This is the field to compare across runs when you are hunting reductions. | yes |
| currency | string | Currency of the price. GBP throughout. | yes |
| bedrooms | integer | Number of bedrooms. | yes |
| bathrooms | integer | Number of bathrooms. | yes |
| receptions | integer | Number of reception rooms. | yes |
| propertySize | string | Floor area as published. On the rows that carry it, it arrives as a bare number with no unit, and most rows do not carry it at all, so do not build a required column on it. | yes |
| tenure | string | freehold, leasehold or share of freehold, lowercased as the source writes it. Missing on a fair share of rows. | yes |
| availability | string | The string true or false rather than a human-readable status. It is a flag, despite the field name suggesting a label. | yes |
| listingLabel | string | Marketing label such as Sold STC or Under offer. Only appears when the listing actually carries one, and it was absent across the rows sampled. | yes |
| features | string[] | The agent's bullet points, verbatim and unstructured. | yes |
| tags | string[] | Category tags such as chain free. Not populated on the rows sampled, so treat it as a bonus rather than a filter. | yes |
| description | string | Full listing copy as the agent wrote it, formatting quirks and all. | yes |
| images | string[] | Photo URLs from the listing, each with a width suffix appended to the URL string. | yes |
| floorPlans | string[] | Floor plan links. Often a link to the listing's floor plan tab rather than a direct image file. | yes |
| uprn | string | Unique Property Reference Number, the UK government identifier for an address and the join key to Land Registry, council tax and planning data. Present on ordinary housing stock but not on every row, so plan a fallback. | yes |
| deposit | string | Deposit required. Rental listings only, and only where the letting agent published one. | yes |
| serviceCharge | string | Annual service charge. Leasehold listings only, and frequently absent even then. | yes |
| groundRent | string | Annual ground rent. Leasehold listings only, on the same caveat as service charge. | yes |
| listingHistory | object[] | The dated price events on the property, each with event, date, price and priceValue. Events seen include Listed, Reduced and Sold, and this is the field sold-price research actually runs on. | yes |
| lastSoldPrice | string | The most recent Sold event's price, lifted out of the history for convenience. Absent when the property has no recorded sale, which is normal for new builds. | yes |
| lastSoldDate | string | Date of that sale, as published, usually month and year rather than a full date. It is text, not ISO, so parse it before sorting. | yes |
| lastSoldValue | number | Numeric form of the last sold price. This is the field to filter on before averaging anything. | yes |
| agentName | string | Estate or letting agent marketing the property. | yes |
| agentPhone | string | Agent contact number where the listing publishes one. It did not come through on the rows sampled, so do not plan a call list around it. | yes |
| countryCode | string | Always GB for this source. | yes |
| summary | string | One-line plain-language summary combining type, address, asking price and last sale, so an agent can read a record without post-processing. | yes |
| error_message | string | Why a search or URL produced no listing, in plain language. Error rows only. | yes |
| error_type | string | Machine-readable error category, for example CollectionError. Error rows only. | yes |
| fetched_at | string | UTC timestamp when the row was collected, in ISO form. The one reliably sortable date on the record. | no |
Worked examples
{
"mode": "search",
"locations": ["Rochdale"],
"propertyType": "For sale",
"maxResultsPerSearch": 50
}
{
"mode": "search",
"locations": ["Rochdale"],
"maxResultsPerSearch": 2
}
{
"mode": "search",
"locations": ["Manchester"],
"propertyType": "to rent",
"maxResultsPerSearch": 50
}
{
"mode": "search",
"locations": ["OL12", "SW19"],
"maxResultsPerSearch": 100
}
{
"mode": "search",
"locations": [
"Rochdale",
"Oldham",
"Bury"
],
"propertyType": "For sale",
"maxResultsPerSearch": 200
}
{
"mode": "url",
"listingUrls": [
"https://www.zoopla.co.uk/for-sale/details/70820128/"
]
}
POWER-USER TIP
The two listing types are capitalised differently — The accepted values are the literal strings "For sale" and "to rent". Nothing normalises them for you, so "To rent" is not a valid value and a run that guesses at the casing will not do what you meant. Sale is the default, so you only ever type the rental one.
POWER-USER TIP
Sale history is the point, and it is not universal — listingHistory carries Listed, Reduced and Sold events with a price on each, and in live runs Sold events went back to March 1998. But a new build has never been sold, and some listings carry no recorded history at all, in which case lastSoldPrice, lastSoldDate and lastSoldValue are simply absent rather than zero. Filter on a non-null lastSoldValue before you average anything.
POWER-USER TIP
UPRN is the join key, but plan a fallback — UPRN is what makes these rows joinable to Land Registry, council tax and planning datasets without fuzzy address matching, and it comes through on ordinary housing stock. It does not come through on every row. Split your pipeline into a clean UPRN join and an address-matched remainder rather than assuming the column is complete.
POWER-USER TIP
Search by location, because saved URLs go dead — UK listings are pulled within weeks of a sale or let, so a listing URL you saved last month often points at nothing. Search mode always returns what is live right now. Keep url mode for a shortlist you are enriching the same day, and note that a removed listing returns an error row and is not charged.
POWER-USER TIP
Start large runs asynchronously — A location search takes a couple of minutes, and the Actor pushes results in chunks as it goes. The run-sync endpoint gives up at 300 seconds, which will cut a large multi-location run off before it finishes. Start the run with POST /runs and poll the run status instead, then read the dataset when it succeeds.
POWER-USER TIP
What is deliberately not here — No council tax band, no EPC rating and no automated valuation. Those are left out rather than shipped as columns that are always empty. If you want an estimate of value, build it from lastSoldValue and priceValue across comparable properties in the same postcode area, which is what the price history is for.
POWER-USER TIP
Volume moves the per-listing price — The listing fee runs from $0.004 on the free tier down to $0.00343 at gold, with the run start and each dataset row costing $0.00001 on top. maxResultsPerSearch is the setting worth getting right first, because it is applied per location and multiplies by however many locations you passed.
Coverage
2
listing types — For sale · to rent
20
locations per run — towns · boroughs · postcode areas
2,000
listings per location — hard cap, and your budget dial
500
listing URLs per run — url mode
37
fields per listing — price history · UPRN · tenure · agent +
2
named dataset views — overview · soldPrices
Tasks
Saved runs with the inputs already filled in — each one a standalone page and a working configuration example.
Written about this source
Walkthroughs, worked examples and posts about this source.
TASK
Check Sold House Prices for Any UK Town or Postcode
TASK
Zoopla Property Data API: UK Listings as JSON
TASK
Compare Asking Price to Last Sold Price by Town
TASK
Track UK Asking Price Changes in a Town Weekly
TASK
Export a UK House Price Dataset to CSV or Excel
TASK
Get UK Sold House Prices in Claude via MCP
TASK
查询英国城镇房屋成交价与历史价格
TASK
获取英国在售房源与价格数据
Code
curl
curl -X POST "https://api.apify.com/v2/acts/johnvc~zoopla-property-api/run-sync-get-dataset-items" \
-H "Authorization: Bearer $APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"mode":"search","locations":["Rochdale"],"propertyType":"For sale","maxResultsPerSearch":50}'
Python
from apify_client import ApifyClient
client = ApifyClient("APIFY_TOKEN")
run = client.actor("johnvc/zoopla-property-api").call(
run_input={
"mode": "search",
"locations": ["Rochdale"],
"propertyType": "For sale",
"maxResultsPerSearch": 50,
}
)
for row in client.dataset(run.default_dataset_id).iterate_items():
if row.get("result_type") != "listing":
continue
print(row["address"], row.get("priceValue"), row.get("lastSoldValue"), row.get("uprn"))
Sold-price dataset view
# Address, last sold price and date, the numeric sold value, the full dated # history, and the UPRN to join on. curl -H "Authorization: Bearer $APIFY_TOKEN" \ "https://api.apify.com/v2/datasets//items?view=soldPrices"
Long run, started asynchronously
# The run-sync endpoint gives up at 300 s. Anything larger than a single small
# town should be started and polled.
from apify_client import ApifyClient
client = ApifyClient("APIFY_TOKEN")
run = client.actor("johnvc/zoopla-property-api").start(
run_input={"mode": "search", "locations": ["Rochdale", "Oldham"], "maxResultsPerSearch": 500}
)
run = client.run(run["id"]).wait_for_finish()
rows = list(client.dataset(run["defaultDatasetId"]).iterate_items())
Changelog
2026-08-08
0.0.9 Every text field is coerced to a string before its row is pushed, so a listing whose price or floor area arrived as a number no longer drops out of the dataset. Re-verified end to end through MCP.
2026-08-07
0.0.6 Published to the Store with eight saved example tasks, covering sold-price lookup, weekly asking-price tracking and the MCP walkthrough.
2026-08-06
0.0.5 Pay-per-listing pricing set with the four discount tiers, and an uncapped run budget no longer aborts the run.
2026-08-06
0.0.1 First build. Location search and URL mode, dated price history with the last sold price and UPRN lifted out, and the overview and sold-price dataset views.
What people use it for
- UK sold house prices by town or postcode
- Building a comparable-sales dataset
- Joining property records to Land Registry by UPRN
- Comparing asking price against last sold price
- Monitoring new listings and price reductions in an area
- UK property data inside an AI agent
Alternatives
Realestate.com.au Property API — the same shape of listing and sold-price data for Australia, searched by suburbours
Google Maps Places Scraper - Fast & Cheap — schools, shops and transport around an address, which no listing page gives youours
Google Maps Directions API — turns a shortlist into commute times, which is how buyers actually rank areasours
Google Local API — finds the agents and trades operating in an area, when the agent field is the thing you are afterours
HM Land Registry Price Paid Data — free, official and a settled figure rather than a published one, but months behind the market and with no live asking prices to compare againstcompeting
azzouzana/zoopla-scraper on the same marketplace — advertises agent email addresses, which this source does not return at allcompeting
jungle_synthesizer/rightmove-zoopla-onthemarket-uk-scraper on the same marketplace — covers three UK portals rather than one, so it wins the moment your question crosses portalscompeting
tri_angle/fast-zoopla-properties-scraper on the same marketplace — the most used option by a distance, though the Store reports roughly a third of its public runs succeeding and it wants a search URL you build yourselfcompeting