How to scrape Zillow
Extract Zillow listing data (price, beds, baths, sqft, address, home type, Zestimate) as typed JSON from a single property URL, no CSS selectors.
- Site:
- zillow.com
- Difficulty:
- Hard
- Rendering:
- JavaScript-rendered
- Anti-bot:
- PerimeterX (HUMAN Security), HUMAN Challenge CAPTCHA, Behavioral scoring (mouse, timing, navigation), Access Denied / press-and-hold interstitial
What you can extract from Zillow
These are the fields teams most often pull from zillow.com, and why a hand-written selector for each one tends to break over time.
| Field | Type | Why selectors break |
|---|---|---|
| price | number | The list price sits inside a hashed, build-generated class like span[data-testid='price'] that React rewrites on every deploy, and Zillow shows different price nodes for active, pending, sold and pre-foreclosure states, so one selector silently grabs the wrong number or null. |
| beds | number | Bed counts live in an unlabeled flex row of bed/bath/sqft facts where order and DOM position shift between condos, lots and multi-family units, so an :nth-child selector that works on a house pulls the bath value on a duplex. |
| baths | number | Baths are rendered as a bare number next to a sibling icon with no stable text label, and half-baths are sometimes folded into a combined 'ba' string, so a selector targeting the visible node returns '2' when the real value is 2.5. |
| sqft | number | Square footage appears with a comma-formatted string and a trailing 'sqft' unit inside the same hashed fact row as beds and baths, and is absent entirely on land listings, so a positional CSS selector breaks the moment the listing has no living area. |
| address | string | The address is split across an h1 plus separate city/state/zip spans, and Zillow A/B tests whether the full street line renders client-side after hydration, so a selector reading only the h1 captures a partial address or empty text on the first paint. |
| homeType | string | Home type (Single Family, Condo, Townhouse, Lot) is buried in a 'Facts and features' accordion that mounts lazily on scroll, so a CSS selector run before the section hydrates finds nothing, and the label text varies by property category. |
| zestimate | number | The Zestimate loads from a separate client-side request after initial render and is hidden behind a tooltip widget with rotating data-testid attributes, so a selector either fires before the value arrives or matches the Rent Zestimate instead of the value Zestimate. |
The schema-first approach
Instead of maintaining selectors, you hand Extracto a JSON schema describing the shape you want. Here is a schema that works for Zillow:
{
"price": "number",
"beds": "number",
"baths": "number",
"sqft": "number",
"address": "string",
"homeType": "string",
"zestimate": "number"
} - 1
Describe the Zillow fields you want
Write a small JSON schema listing the fields to pull from zillow.com (for example price, beds, baths). No CSS selectors or XPath needed.
- 2
Send the URL and schema to Extracto
POST the Zillow page URL and your schema to the Extracto API. Extracto renders the page in a real browser, handling JavaScript and dynamic content automatically.
- 3
Get schema-validated JSON back
Extracto returns JSON that matches your schema exactly, validated before it leaves the API, so your pipeline never receives broken or partial data.
Common pitfalls when scraping Zillow
- Zillow ships listing data twice: once in server-rendered HTML and again in an embedded Apollo/__NEXT_DATA__ JSON blob that hydrates the page. The two can disagree (the HTML may show a cached price while the JSON holds the live one), so extracting against the wrong layer yields stale numbers. Schema-driven extraction over the fully rendered page sidesteps the guesswork.
- The Zestimate and Rent Zestimate are distinct values that render in near-identical widgets, and on for-sale homes the value Zestimate may be suppressed in favor of the list price. Confusing the two, or capturing a Zestimate that has not finished loading, produces a field that looks plausible but is wrong. Define zestimate explicitly in your schema so the unrelated rent figure is never mapped into it.
- Land and lot listings have no beds, baths or sqft, and pending or sold homes change which price node is authoritative. A pipeline that assumes every property is a house will either crash or backfill a neighboring field's value. Returning null for the genuinely absent fields, rather than guessing, keeps these mixed listing types clean.
- PerimeterX scores behavior over a session, so a burst of rapid requests to /homedetails/ pages trips the HUMAN Challenge even when individual requests look valid. Pulling one URL per call at a modest pace, on a managed rendering layer, is far more durable than hammering the site and getting interstitialed mid-crawl.
Is it legal to scrape Zillow?
Zillow's Terms of Use explicitly prohibit using "any robot, spider, scraper or other automated means" to access the Services without Zillow's express written permission, and its robots.txt disallows the listing paths that matter most, including /homedetails/ and /homes/. Zillow also enforces a press-and-hold HUMAN Challenge and reserves the right to apply technical blocks, account suspension and legal remedies. Treat Zillow data as off-limits for bulk automated collection unless you have a written agreement or use an official channel. Zillow does run a partner and API program (Bridge Interactive / ZGMI) and publishes free aggregate housing datasets through Zillow Research, which are the compliant routes for most analytics use cases. If you extract individual public listing pages at all, keep volume minimal, respect robots.txt and the ToS, never access login-gated or saved-search data, and consult counsel before any commercial use. This guide is educational and does not constitute legal advice; the lawful path for production real-estate data is Zillow's own data programs, not scraping.
Extracto follows robots.txt by default and rate-limits politely. You are responsible for compliance with Zillow's terms and applicable law (GDPR, CCPA) in your use case.