Zillow Scraper API: How to Build a Real Estate Property Monitor
If you need to track property listings, price changes, or listing status over time, the most practical starting point is a Zillow scraper API workflow that converts public listing pages into structured, time-stamped records. This article is for developers, data analysts, and real estate operations teams who want to build a property monitor from repository-based Python examples rather than maintaining a full headless-browser stack themselves.
Quick answer
A real estate property monitor does three things:
- Collect structured fields from public Zillow listing pages on a schedule you define.
- Store each observation with a collection timestamp and source URL so you can detect changes.
- Compare new records against previous ones to surface price drops, status changes, and new listings.
The data-scrape GitHub organization hosts two relevant reference repositories for this workflow: the Zillow Scraper API project and the companion Zillow Data Scraper. Treat these as implementation references, not guaranteed production services. Verify the README, code, and current maintenance status before relying on any endpoint or field.
Why public real estate data is harder to monitor than it looks
Zillow's public pages are designed for human browsing, not programmatic feeds. Anyone building a monitor quickly runs into the same problems:
- Dynamic rendering. Listing details, photos, and pricing history are loaded by JavaScript after the initial page request. A plain HTTP client often receives incomplete HTML.
- Layout drift. CSS selectors and page structure change without warning, which breaks extraction logic within weeks or months.
- Rate limiting and session behavior. Repeated requests from the same IP or user-agent pattern can trigger blocks, empty responses, or verification challenges.
- Data freshness. A listing may already be under contract, removed, or re-listed by the time your monitor runs. The page timestamp and your collection timestamp are different facts.
- Duplicate and variant records. The same address can appear as a new listing, a price change, or a re-post, so a monitor needs de-duplication rules.
These constraints make self-hosted scraping viable for prototypes but expensive to maintain at scale. A repository-based scraper API workflow gives you a middle ground: you run the code, control the schema, and decide how much infrastructure to own.
What a Zillow scraper API returns
A "scraper API" in this context is a wrapper around the extraction layer. You send a property URL, search parameters, or location query; the API returns normalized JSON. The exact fields depend on the repository implementation and the page type, but typical listing-level output includes:
Property identity
-
zillow_idor listing identifier -
address,city,state,zip_code -
latitude,longitudewhen available -
property_url(the canonical source URL)
Listing details
-
price,price_history(when exposed on the page) -
bedrooms,bathrooms,square_footage -
listing_status: for sale, pending, sold, for rent, etc. -
days_on_marketor listing date when visible
Media and source
-
photo_urls(first image or gallery list) -
descriptiontext from the public listing -
agent_nameor brokerage when publicly displayed
Provenance
-
collected_at: the time your workflow observed the page -
source: the public page origin -
review_status: a label for human validation
Because public pages differ by region, device type, and whether the listing is active, treat any field as optional until you have validated it against a controlled sample.
Step-by-step: building the monitor
The workflow below assumes you have installed one of the repository-based tools and configured an endpoint or local runner according to its documentation. It uses environment variables for anything account-specific.
1. Set environment variables
export ZILLOW_API_ENDPOINT="https://your-endpoint-or-localhost.example.com/scrape"
export ZILLOW_API_KEY="your_api_key_if_required"
export TARGET_ZIP="90210"
export MONITOR_OUTPUT_DIR="./data"
Replace the endpoint with the current value shown in the repository README or your own deployment.
2. Request listing data
import json
import os
import hashlib
from datetime import datetime, timezone
from pathlib import Path
import requests
API_ENDPOINT = os.environ["ZILLOW_API_ENDPOINT"]
API_KEY = os.environ.get("ZILLOW_API_KEY")
OUTPUT_DIR = Path(os.environ.get("MONITOR_OUTPUT_DIR", "./data"))
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
headers = {"Content-Type": "application/json"}
if API_KEY:
headers["Authorization"] = f"Bearer {API_KEY}"
# Example payload; confirm the exact schema in the repository README.
payload = {
"query": "for-sale",
"location": os.environ.get("TARGET_ZIP", "90210"),
"max_results": 25,
}
response = requests.post(API_ENDPOINT, headers=headers, json=payload, timeout=120)
response.raise_for_status()
raw_listings = response.json()
print(f"Retrieved {len(raw_listings.get('data', []))} listings")
3. Normalize and store records
def clean_text(value):
if not isinstance(value, str):
return None
value = " ".join(value.split())
return value or None
def as_positive_int(value):
if isinstance(value, bool):
return None
if isinstance(value, int) and value > 0:
return value
if isinstance(value, str):
digits = "".join(c for c in value if c.isdigit())
if digits:
return int(digits)
return None
def normalize_listing(raw, collected_at):
address = clean_text(raw.get("address") or raw.get("streetAddress"))
if not address:
return None
price = as_positive_int(raw.get("price") or raw.get("unformattedPrice"))
record = {
"listing_id": clean_text(raw.get("zpid") or raw.get("listing_id") or raw.get("id")),
"address": address,
"city": clean_text(raw.get("city")),
"state": clean_text(raw.get("state")),
"zip_code": clean_text(raw.get("zipcode") or raw.get("zip")),
"price": price,
"bedrooms": as_positive_int(raw.get("bedrooms") or raw.get("beds")),
"bathrooms": as_positive_int(raw.get("bathrooms") or raw.get("baths")),
"square_feet": as_positive_int(raw.get("livingArea") or raw.get("square_feet")),
"status": clean_text(raw.get("status") or raw.get("listingStatus")),
"property_url": clean_text(raw.get("detailUrl") or raw.get("property_url") or raw.get("url")),
"photo_url": clean_text(raw.get("imgSrc") or raw.get("photo_url")),
"collected_at": collected_at,
"source": "public Zillow listing page",
"review_status": "needs_review",
}
return record
collected_at = datetime.now(timezone.utc).isoformat()
records = []
seen_ids = set()
for raw in raw_listings.get("data", []):
if not isinstance(raw, dict):
continue
record = normalize_listing(raw, collected_at)
if record and record["listing_id"] and record["listing_id"] not in seen_ids:
records.append(record)
seen_ids.add(record["listing_id"])
output_file = OUTPUT_DIR / f"listings_{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}.json"
output_file.write_text(json.dumps(records, indent=2, ensure_ascii=False), encoding="utf-8")
print(f"Saved {len(records)} normalized records to {output_file}")
4. Detect changes across runs
def record_fingerprint(record):
"""Create a stable key for de-duplication and change detection."""
parts = [
record.get("listing_id") or "",
str(record.get("price") or ""),
record.get("status") or "",
]
return hashlib.sha256("|".join(parts).encode()).hexdigest()[:16]
for record in records:
record["fingerprint"] = record_fingerprint(record)
# A production monitor would compare fingerprints against the previous run
# and emit events for new listings, price changes, and status changes.
This separation keeps extraction, normalization, and change detection independent. If the extraction layer changes, you can update one function without rewriting the monitor logic.
Representative output shape
After normalization, a single record looks like this:
{
"listing_id": "123456789",
"address": "1234 Sunset Blvd",
"city": "Los Angeles",
"state": "CA",
"zip_code": "90210",
"price": 2495000,
"bedrooms": 4,
"bathrooms": 3,
"square_feet": 3100,
"status": "FOR_SALE",
"property_url": "https://www.zillow.com/homedetails/1234-sunset-blvd-los-angeles-ca-90210/123456789_zpid/",
"photo_url": "https://photos.zillowstatic.com/.../image.jpg",
"collected_at": "2026-08-13T02:00:00+00:00",
"source": "public Zillow listing page",
"review_status": "needs_review",
"fingerprint": "a1b2c3d4e5f67890"
}
A null field means the value was not present or not parseable in that run, not that it is zero or empty. Keeping that distinction visible prevents bad downstream decisions.
Use cases for a property monitor
Price-drop alerts. Compare daily fingerprints and notify a buyer or investor when a listing price decreases.
Market velocity tracking. Count new listings, pending sales, and sold listings per ZIP code over time to estimate inventory turnover.
Rental arbitrage research. Monitor rental listings, price per square foot, and days on market for specific neighborhoods.
Investment screening. Filter by bedroom count, square footage, and status to build a shortlist for human review.
Comparable sales analysis. Collect recently sold records with price and property attributes to support valuation models.
Each use case should start with a narrow scope, clear data retention rules, and a manual review gate before any financial or legal decision.
Repository workflow versus building from scratch
| Dimension | Repository-based scraper API | Build your own from scratch |
|---|---|---|
| Setup time | Hours to days, depending on the repo | Days to weeks for a reliable prototype |
| Maintenance | You update selectors and dependencies | You own the full extraction and anti-detection stack |
| Schema control | You define the normalization layer | You define everything |
| Proxy/headless overhead | Depends on repo design; may still need proxies | You manage browsers, proxies, and rotation |
| Field certainty | Validate from actual test outputs | Validate from your own parsing logic |
| Compliance | Your responsibility either way | Your responsibility either way |
Neither option removes the obligation to respect Zillow's terms, applicable law, and privacy requirements. A repository gives you a head start on code; it does not give you permission to collect or use data indiscriminately.
Limitations and compliance
- No official Zillow API guarantee. Public pages are not a stable data contract. Fields, URLs, and availability can change.
- Regional variation. The same query may return different fields or layouts depending on location, device fingerprint, and session.
- Rate and volume limits. Running a monitor too aggressively can trigger blocks. Start with a small cadence and increase only after observing stable behavior.
- Data accuracy. Public listings can be stale, duplicated, or re-listed. Always validate high-impact records before acting.
- Legal and platform terms. Use public data only for lawful purposes and in compliance with Zillow's terms of use, robots directives, and applicable privacy laws. Do not evade access controls, logins, or rate limits.
FAQ
Is the repository a managed production API?
No. The Zillow Scraper API repository and Zillow Data Scraper repository are code references. Review their READMEs, dependencies, and recent commits before using them in any system.
What fields are guaranteed?
None. Public page extraction depends on the current page structure, region, and listing type. Validate every field you plan to use with a controlled test sample.
How often should the monitor run?
Start daily. Increase frequency only if the decision you are supporting actually benefits from hourly updates and you can handle the increased block risk.
Can this feed into a CRM, dashboard, or AI agent?
Yes, but only after normalization and review. The JSON output in the example above is designed to be stored in a database, sent to a webhook, or loaded as context for an AI agent. Keep provenance fields so the downstream system knows where each value came from.
What happens when Zillow changes its page layout?
The extraction step may break or return partial records. A well-structured monitor detects increased null rates or parse failures and alerts a human to inspect the repository for updates.
Should I keep every raw response?
Only retain what your use case and governance rules justify. Keep enough to audit a result, but do not accumulate unnecessary personal or property data.
Where do I inspect the referenced projects?
Start with the data-scrape GitHub profile, then review the Zillow Scraper API repository and the Zillow Data Scraper repository.
Next step
Before running a production monitor, define the exact ZIP codes or search criteria you need, write a target JSON schema, collect a small test sample, and assign someone to validate the output. The repositories above are useful starting points for that evaluation; the durable asset is the documented, reviewable workflow your team builds around them.













