ECOMMERCEFINANCIAL API MCP READY

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

PARAMETERTYPEREQDEFAULTDESCRIPTION
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

FIELDTYPEDESCRIPTIONNULLABLE
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

Sold prices for a town — the run most people start from, and the one the price history exists for
{
  "mode": "search",
  "locations": ["Rochdale"],
  "propertyType": "For sale",
  "maxResultsPerSearch": 50
}
Cheap test run — two rows is enough to see the schema and costs about a penny
{
  "mode": "search",
  "locations": ["Rochdale"],
  "maxResultsPerSearch": 2
}
Rentals instead of sales — note the lowercase r, which is the literal enum value
{
  "mode": "search",
  "locations": ["Manchester"],
  "propertyType": "to rent",
  "maxResultsPerSearch": 50
}
A postcode area rather than a town — outward codes work as locations, which is how you keep a comparable set tight
{
  "mode": "search",
  "locations": ["OL12", "SW19"],
  "maxResultsPerSearch": 100
}
Several places in one run — up to 20 locations, and searchLabel tells you which one each row came from
{
  "mode": "search",
  "locations": [
    "Rochdale",
    "Oldham",
    "Bury"
  ],
  "propertyType": "For sale",
  "maxResultsPerSearch": 200
}
Listings you already hold — url mode, for enriching a shortlist the same day you built it
{
  "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.

Request a task →

Written about this source

Walkthroughs, worked examples and posts about this source.

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

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