Redfin Estimate and Sold Comps API: Pull Home Values and Sale History with Python

Published October 7, 2026

A defensible home valuation never comes from one number. The model estimate (AVM) answers "what does the algorithm think it is worth", sold comps answer "what did buyers actually pay nearby", and the property's own sale-and-tax history answers "what happened to this exact home". This guide chains all three through the Redfin Estimate and sold comps API in Python: resolve an address to its propertyId/listingId, pull the Redfin Estimate, cross-check with soldWithin-filtered closed sales, and ground the result in the subject property's recorded timeline.

The Redfin Property Data API is an unofficial data service, not an affiliated Redfin integration. It returns publicly available listing content as JSON for research and informational purposes; values change over time and no single response is a guarantee of current market truth.

Endpoints at a glance

OperationRequired inputRole in the workflow
property-auto-completelocationTurn free text into region IDs and canonical place names
v2-detailslocationResolve a propertyId, address, slug or URL into the full payload and canonical IDs
estimatepropertyId, listingIdThe Redfin Estimate automated valuation
region-search-for-soldregionIdClosed-sale comps inside a region, with a soldWithin window
location-search-for-soldlocationThe same comps around a free-form city, ZIP or address
history-info-sale-and-tax-historypropertyId, listingIdRecorded sale events and tax/assessment history for the subject home

Autocomplete, region search and the v2 detail route were verified live (New York resolves to region 6_30749). Treat every payload as a sample retrieved at request time, not a frozen fixture.

Step 1 — Prepare an authorized platform key

Sign in and copy an active key from your dashboard. The examples read it from the OPENDATA_API_KEY environment variable; substitute YOUR_API_KEY only when you actually run a request.

No Redfin account, cookie or session token is involved — the platform key is the only credential. Because the key travels in the query URL, keep URLs out of shared links, screenshots and logs, and run production calls server-side.

Step 2 — Resolve the address to propertyId and listingId

Focused valuation endpoints key off the propertyId/listingId pair. Two ways to get it:

# Option A: autocomplete, then search
curl --get 'https://api.opendata-api.com/api/project/redfin/property-auto-complete?key=YOUR_API_KEY' \
  --data-urlencode 'location=New York'

# Option B: one v2-details call accepts an ID, address, slug or full URL
curl --get 'https://api.opendata-api.com/api/project/redfin/v2-details?key=YOUR_API_KEY' \
  --data-urlencode 'location=40775351'

Autocomplete returns data.exactMatch plus data.rows; each row carries id (region identifier, e.g. 6_30749), subName (canonical New York, NY, USA form) and url. A text that matches nothing still returns HTTP 200 with hasFakeResults=true and a sentinel row id of 23_null — check hasFakeResults before treating suggestions as real places. The v2 detail payload's initialInfo exposes propertyId and listingId; note that an unknown propertyId on this route currently surfaces as a bare HTTP 500, so treat 5xx as the not-found signal there.

Step 3 — Pull the Redfin Estimate

With the pair in hand, request the model valuation:

curl --get 'https://api.opendata-api.com/api/project/redfin/estimate?key=YOUR_API_KEY' \
  --data-urlencode 'propertyId=40775351' \
  --data-urlencode 'listingId=224185786'

The estimate is the platform's own AVM output — distinct from the asking price in search rows and from recorded sale prices in history. Inspect the returned payload's available fields rather than assuming a fixed schema, and store the retrieval date with any figure you keep.

Step 4 — Cross-check with sold comps

Pull closed sales near the subject with a recency window (soldWithin accepts 7, 30, 90, 180, 365, 730, 1095 or 1825 days) and comp-grade filters:

curl --get 'https://api.opendata-api.com/api/project/redfin/location-search-for-sold?key=YOUR_API_KEY' \
  --data-urlencode 'location=New York, NY, USA' \
  --data-urlencode 'soldWithin=90' \
  --data-urlencode 'numBeds=2' \
  --data-urlencode 'limit=50'

Each row in the data array wraps a homeData object with propertyId, listingId, priceInfo.amount, sqftInfo.amount, beds, baths, lotSize and addressInfo — the same shape as for-sale search, so one parser covers both.

Step 5 — Anchor with sale and tax history

curl --get 'https://api.opendata-api.com/api/project/redfin/history-info-sale-and-tax-history?key=YOUR_API_KEY' \
  --data-urlencode 'propertyId=40775351' \
  --data-urlencode 'listingId=224185786'

Where the sold search answers "what did neighbors pay", this endpoint answers "what happened to this exact home": every recorded sale event and assessment cycle, useful for appreciation curves and flip detection.

Putting it together in Python

This bounded script resolves a place, values one listing, and builds a comp-based per-square-foot band from sold rows. No response is hard-coded; missing fields stay missing.

import os
import statistics
import requests

BASE = "https://api.opendata-api.com/api/project/redfin"
API_KEY = os.environ["OPENDATA_API_KEY"]


def call(operation, **params):
    try:
        response = requests.get(
            f"{BASE}/{operation}",
            params={**params, "key": API_KEY}, timeout=30,
        )
    except requests.RequestException:
        raise RuntimeError("Request failed; use redacted diagnostics") from None
    if not response.ok:
        raise RuntimeError(f"HTTP {response.status_code}")
    gateway = response.json()
    if gateway.get("code") != 200:
        raise RuntimeError("Gateway business failure; inspect code/message safely")
    payload = gateway.get("data")
    if not isinstance(payload, dict):
        raise ValueError("Unexpected operation payload")
    return payload


def find_number(node, needle):
    # Adaptive extraction: the estimate schema is not fixed, so locate
    # the first numeric field whose key contains the needle.
    if isinstance(node, dict):
        for key, value in node.items():
            if needle in key.lower() and isinstance(value, (int, float)):
                return value
            found = find_number(value, needle)
            if found is not None:
                return found
    elif isinstance(node, list):
        for item in node:
            found = find_number(item, needle)
            if found is not None:
                return found
    return None


ac = call("property-auto-complete", location="New York")
rows = ((ac.get("data") or {}).get("rows")) or []
if (ac.get("data") or {}).get("hasFakeResults") or not rows:
    raise ValueError("No real place matched; refine the input text")
region_id = rows[0]["id"]  # e.g. 6_30749

sale = call("region-search-for-sale", regionId=region_id, limit=10)
homes = [row.get("homeData") or {} for row in (sale.get("data") or [])]
subject = next((h for h in homes if h.get("propertyId") and h.get("listingId")), None)
if subject is None:
    raise ValueError("No listing with a complete propertyId/listingId pair")

estimate_payload = call(
    "estimate", propertyId=subject["propertyId"], listingId=subject["listingId"])
avm = find_number(estimate_payload, "estimate")
print("Redfin Estimate:", "unavailable" if avm is None else avm)

sold = call("region-search-for-sold", regionId=region_id, soldWithin=90, limit=50)
comps = []
for row in (sold.get("data") or []):
    home = row.get("homeData") or {}
    price = (home.get("priceInfo") or {}).get("amount")
    sqft = (home.get("sqftInfo") or {}).get("amount")
    if price and sqft:
        comps.append(price / sqft)
if comps:
    median = statistics.median(comps)
    print(f"Comps: {len(comps)} sales, median ${median:,.0f}/sqft, "
          f"range ${min(comps):,.0f}-${max(comps):,.0f}/sqft")
    subject_sqft = (subject.get("sqftInfo") or {}).get("amount")
    if subject_sqft:
        print(f"Comp-implied value: ${median * subject_sqft:,.0f}")

history = call("history-info-sale-and-tax-history",
               propertyId=subject["propertyId"], listingId=subject["listingId"])
print("History payload keys:", sorted(history.keys()))

The same flow from JavaScript is one fetch per step — for example fetch(\https://api.opendata-api.com/api/project/redfin/estimate?key=${key}&propertyId=${pid}&listingId=${lid}\`)` — keeping the key in server-side environment configuration, never in shipped client code.

HTTP status versus business status

Two layers matter. The HTTP status reflects transport; the gateway envelope carries code, message and data, where code 200 indicates gateway business success — HTTP 200 alone does not. Search payloads add their own status_code and resultsPerPage next to the data array. Autocomplete has no status field: hasFakeResults=true with a 23_null row is a valid no-match, and the v2 detail route reports an unknown propertyId as HTTP 500. Program against both layers, and page searches with page/limit (up to 350 rows), deduplicating propertyId/listingId pairs across pages.

Use cases

  • AVM benchmarking — compare the Redfin Estimate against your own model and against closed comps before trusting either.
  • Comp-based valuation tools — compute a defensible value range from soldWithin-windowed, attribute-matched sales.
  • Pre-offer pricing checks — flag listings priced far above both the estimate and the comp band.
  • Portfolio tracking — re-pull estimates periodically and plot appreciation against the history timeline.
  • Flip detection — match repeat sale events of the same propertyId across windows.

FAQ

Is there an official Redfin API for developers?

No public official API exists. This is an unofficial data service that returns publicly available Redfin content as documented JSON, so you do not maintain scrapers or proxies.

How do I scrape Redfin listings with Python without getting blocked?

Don't scrape; call the search endpoints. Region and location searches return structured rows with paging, which removes the IP-rotation and HTML-parsing burden entirely.

How can I get the Redfin Estimate for a property programmatically?

Resolve the property to its propertyId/listingId pair (search rows or v2-details), then call estimate with both, as in Step 3.

How do I pull recently sold home comps by ZIP code?

Pass the ZIP as location to location-search-for-sold with a soldWithin window — 90 days is a common comp default.

How do I find a Redfin region ID to search listings by region?

Call property-auto-complete with the place name and read rows[].id (e.g. 6_30749 for New York). Never invent IDs; hasFakeResults=true means no real place matched.

How do I get a property's full sale and tax history from an API?

Call history-info-sale-and-tax-history with the propertyId/listingId pair from the same search row or detail lookup.

How do I convert a Redfin listing URL into structured JSON?

Pass the full link to v2-details (or detail-by-url for a lighter record). For a whole search-results page, property-search-by-url replays the saved search URL as JSON.

What is the best alternative to the Zillow API for US listings?

It depends on the data you need. This project covers Redfin's estimate, live inventory, sold comps and per-property detail; run both against the same addresses and compare coverage for your markets.

How do I build a home valuation tool using sold comps and AVM estimates?

Combine the three sources this guide chains: the AVM for a model value, soldWithin-filtered comps for market evidence, and sale-and-tax history for the subject's own record. Present a range, not a point.

What data can I get from a Redfin property details endpoint?

v2-details returns the complete payload (identity, price, location, photos and detail sections) in one call; focused routes add schools, walk scores, amenities, climate risk, popularity, price history and more.

Try the estimate, sold search and sale-and-tax history references, or start from the Redfin project page. APIs for similar platforms — Zillow, Realtor.com, Trulia, Homes.com, Apartments.com, ATTOM Data, Rentcast, Estated, PropStream, Mashvisor and others — can be added on request through the contact page; private source-code deployment is also available.