DEV Community

coreclaw
coreclaw

Posted on

Zillow Scraper API: How to Build a Real Estate Property Monitor

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:

  1. Collect structured fields from public Zillow listing pages on a schedule you define.
  2. Store each observation with a collection timestamp and source URL so you can detect changes.
  3. 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_id or listing identifier
  • address, city, state, zip_code
  • latitude, longitude when 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_market or listing date when visible

Media and source

  • photo_urls (first image or gallery list)
  • description text from the public listing
  • agent_name or 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"
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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}")
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)