How to Scrape Zillow Real Estate Data: Search JSON, Zestimate, PerimeterX
Zillow ships its listing data as JSON hidden in the page — you never need to parse a single div. Here's how to drive the map-bounds search API, beat the pagination cap, and survive PerimeterX.
Zillow holds more than 110 million properties and draws around 245 million monthly visitors, which makes it the default source for US real-estate data — prices, addresses, beds, baths, zestimates and rent estimates. It also sits behind PerimeterX and renders client-side, so naive scrapers get a 403 and a blank page. The good news: Zillow hands you its data as JSON if you know where to look, and its search is driven by map coordinates you can call directly. Here is the whole approach.
Don't parse HTML — Zillow ships JSON
There is no need to fight CSS selectors on a Zillow property page. The full property record is embedded as hidden web data in a script tag. Two locations cover almost every page: <script id="__NEXT_DATA__"> holds the Next.js cache, and some pages instead use <script id="hdpApolloPreloadedData"> (the Apollo GraphQL cache). Grab either, parse it, and you have a clean object with zpid, price, livingArea, bedrooms, bathrooms, latLong, homeStatus, rentZestimate and more:
import json, re, requests
proxy = "http://USER:PASS@gate.quantumproxies.io:8000"
proxies = {"http": proxy, "https": proxy}
HEADERS = {"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36",
"accept-language": "en-US,en;q=0.9"}
def property_json(url):
r = requests.get(url, headers=HEADERS, proxies=proxies, timeout=20)
r.raise_for_status() # 403 => blocked, rotate IP
m = re.search(r'id="__NEXT_DATA__"[^>]*>(.*?)</script>', r.text, re.S)
if m:
data = json.loads(m.group(1))
cache = data["props"]["pageProps"]["componentProps"]["gdpClientCache"]
return next(iter(json.loads(cache).values()))["property"]
# fallback: Apollo cache
m = re.search(r'id="hdpApolloPreloadedData"[^>]*>(.*?)</script>', r.text, re.S)
cache = json.loads(json.loads(m.group(1))["apiCache"])
return next(v["property"] for k, v in cache.items() if "ForSale" in k)
The embedded record is far richer than the visible page. Alongside price and address you get zestimate and rentZestimate — Zillow's own sale and rent valuations — plus homeStatus (for sale, sold, for rent), daysOnZillow, lot and living area, and precise latitude and longitude. That is everything a comps model, a lead-scoring pipeline or a market dashboard needs, delivered as one clean JSON object per property instead of a dozen brittle CSS selectors that break on the next redesign.
The search API runs on map coordinates
Zillow's search is not keyword-first — it is map-first. Under the hood, submitting a search fires a request to zillow.com/async-create-search-page-state with a searchQueryState object whose core field is mapBounds: four numbers (north, south, east, west) that draw a rectangle on the map. Give it a bounding box and it returns every listing inside, as structured JSON:
def search_area(bounds, page=1):
url = "https://www.zillow.com/async-create-search-page-state"
body = {
"searchQueryState": {
"pagination": {"currentPage": page},
"mapBounds": bounds, # {'north':..,'south':..,'east':..,'west':..}
"filterState": {"sortSelection": {"value": "days"}},
},
"wants": {"cat1": ["listResults", "mapResults"], "cat2": ["total"]},
"requestId": 2,
}
r = requests.put(url, json=body, headers=HEADERS, proxies=proxies, timeout=20)
return r.json()["cat1"]["searchResults"]["listResults"]
# New Haven, CT bounding box
listings = search_area({"north": 41.366, "south": 41.230,
"east": -72.827, "west": -73.030})

Beat the 820-result ceiling
Zillow only paginates 20 pages of about 41 items, so a single search caps at roughly 820 results no matter how many homes are in the area. Dense markets hide thousands more. The workaround is recursive quadrant splitting: if a bounding box returns the maximum pages, split it into four smaller boxes and search each; keep subdividing until every box returns fewer than the cap. This zoom-in strategy can surface hundreds of thousands of listings from one metro that the flat search would never show — and it only subdivides where the density warrants it, so you do not waste requests on empty countryside.
There is a cost to that thoroughness: each subdivision multiplies requests, so a dense metro can turn one search into dozens. Budget for it — cap the recursion depth and only zoom where the result count justifies it, which the algorithm does for you by subdividing solely when a box still hits the maximum pages. For a nightly refresh of a whole city, most requests then land on the handful of dense downtown boxes while the suburbs resolve in a single pass, so you spend on depth exactly where the inventory is and nowhere else.
PerimeterX is why naive scrapers fail
Zillow's blocking is not primarily rate-based; it is PerimeterX (now HUMAN) profiling the request. Datacenter IPs, missing browser headers and inhuman timing all trigger a 403 or a challenge — that is what an assert status == 200 is really checking. The fix is a coherent, trusted request: US residential proxies so the exit IP looks like a homebuyer, realistic browser headers, and exponential backoff that rotates to a fresh IP on any 403 rather than retrying the burned one. Our breakdown of how PerimeterX detects automation covers the signals in depth; the same posture applies to Walmart, which uses the same vendor.
When PerimeterX tightens or you would rather not maintain a browser fleet, a Scraper API renders the page, rotates residential IPs and clears the challenge for you, returning the same hidden JSON without the operational overhead.
Scrape Zillow behind PerimeterX with residential IPs

When to use county records instead
Zillow is superb for listings, photos and estimates, but it is a secondary source. For authoritative ownership, sale history and assessed values, county assessor and recorder offices publish public records — often via their own portals or bulk data exports. If your use case is title, comps or tax data rather than live listings, county-level records are cleaner, more permissive to collect, and free of PerimeterX entirely. Use Zillow for the market signal and county records for the ground truth.
Is scraping Zillow legal?
This is general information, not legal advice. Publicly available listing data — prices, addresses, property attributes — is broadly considered fair to collect at respectful rates. Zillow results can include personal data, though: agent names, phone numbers and broker details. Those are protected under GDPR and similar laws, so avoid harvesting and storing them without a lawful basis. Collect the property facts, not the people.
Frequently asked questions
Can I scrape Zillow data?
Yes — publicly visible listing data can be collected. Zillow embeds each property record as JSON in a script tag (__NEXT_DATA__ or the Apollo cache), and its search runs on a map-bounds API you can call directly. The obstacle is not access but PerimeterX, so you need residential IPs and coherent headers to avoid 403s.
Does Zillow have an API?
Zillow offers roughly 20 official APIs, including Property Details and Neighborhood Data, but they require keys, carry usage limits and do not cover every field or bulk use case. Many teams scrape the public pages instead precisely because the hidden JSON already contains the price, beds, baths, area, zestimate and rent estimate they need.
Why does my Zillow scraper get a 403?
A 403 on Zillow is almost always PerimeterX, not a rate limit. Datacenter IPs, missing or inconsistent browser headers, and robotic request timing all trigger it. Switch to US residential proxies, send realistic headers, add human-like spacing, and rotate to a fresh IP on every 403 with exponential backoff rather than hammering the blocked one.
How do I scrape more than 820 Zillow listings from one area?
A single search caps at about 820 results (20 pages of 41). To go deeper, split the map bounding box into four quadrants and search each; recursively subdivide any quadrant that still hits the cap. This zoom-in approach only subdivides dense areas, so it captures the full inventory of a metro without wasting requests on sparse regions.
Skip the HTML and read the embedded JSON, drive the search by map bounds, split quadrants to beat the cap, and route everything through clean residential IPs to stay ahead of PerimeterX. Do that and Zillow becomes a structured real-estate feed instead of a wall of 403s.