Redfin Comparable Sales

Site redfin.comTask get-comparable-sales-lzph4dVersion v1Updated Jul 13, 2026Category real-estate

Return recent comparable sales (sold comps) on Redfin for a subject property, with every filter dimension the Recently Sold UI exposes (recency, distance, price, beds, baths, sqft, lot, year built, property type, days-on-market, sort). Read-only. This skill was captured from a live agent session on redfin.com and publishes here verbatim, exactly as an agent receives it.

NoteSelectors and URL schemes drift as sites change. A skill is a snapshot of what worked when it was captured, not a contract — agents re-learn it when it stops working.

Purpose

Given a subject property (Redfin URL, MLS/property ID, full street address, or lat+lon), return recent comparable sales ("comps") in the surrounding area as structured JSON. Every dimension Redfin's "Recently Sold" filter exposes is mapped to the underlying query-string keys (recency, distance, price, beds, baths, sqft, lot, year built, property type, days-on-market, sort order, pagination). The skill also returns the subject's address, lat/lon, Redfin Estimate, and most-recent sale info so the caller can frame the comp set against the subject. Read-only — never click Save, Tour, Contact Agent, Get Pre-Approved, Sign In, or any mutation control.

When to Use

  • Pulling 3–25 recent local sales to support an AVM, broker price opinion, or appraisal review.
  • Bulk extraction across many subjects (one comp set per address) for portfolio analysis or property-tax challenges.
  • Surfacing "what did houses like this go for" for a buyer/seller chat agent.
  • Anywhere you'd otherwise screenshot Redfin's Recently-Sold map UI — the JSON endpoint is 10–50× faster, cheaper, and exposes the full property record (beds/baths/sqft/lot/year/photos/MLS#/agent) per result.

Workflow

Redfin's web UI is a thin client over a public-but-undocumented JSON endpoint at https://www.redfin.com/stingray/api/gis?.... No auth or cookies required, but a residential proxy is mandatory — Redfin's edge throttles non-residential IPs aggressively (intermittent 403/captcha on 2–3 sequential requests from the same datacenter IP). The optimal surface is a single browserless_agent call that gotos the gis URL directly with a residential proxy and reads the JSON out of the page body in-page; the rendered web UI is only used as a fallback when the proxy pool is exhausted, when the subject must be resolved from a raw street address (autocomplete is cookie-walled), or when the Redfin Estimate / list-price-vs-sale delta must be surfaced (those are not in the gis JSON — they only live on the property detail page).

  1. Resolve the subject into {propertyId, regionId, regionType, market, lat, lon, zip}.

    • Redfin URL given → propertyId is the trailing path segment after /home/. Open the URL with a residential-proxy browserless_agent goto (a real browser follows Redfin's slug-rewrite redirect natively) and evaluate over the JSON-LD <script type="application/ld+json"> blocks in-page: mainEntity.address, mainEntity.geo.{latitude,longitude}, mainEntity.numberOfBedrooms, mainEntity.numberOfBathroomsTotal, mainEntity.floorSize.value. The breadcrumb block (BreadcrumbList) carries the city's region URL /city/{regionId}/{ST}/{City} — pull {regionId} from there. (Region type for city is 6.) The Redfin Estimate is in the rendered HTML as <div class="price smallerFont">$X,XXX,XXX</div> inside id="redfin-estimate" — read it in the same evaluate.
    • Property ID given → fetch https://www.redfin.com/CA/-/home/{id} (Redfin rewrites the slug; follow redirects), then the same JSON-LD extraction.
    • Lat/lon given → use a poly= rectangle of ±delta around the point (see step 3). No region required. Pick market= from the lat/lon's metro (e.g. sanfrancisco, losangeles, seattle, boston, newyork, chicago, dallasfortworth, dc); market is a required argument on the gis endpoint and gates the sold-filter behavior.
    • Street address given → Redfin's /stingray/do/location-autocomplete?location=... returns 403 without a warm session. The direct gis-navigation path cannot resolve a raw street address to a propertyId. Browser-fallback: a single residential-proxy browserless_agent call whose commands array chains a goto (url: "https://www.redfin.com/"), a type of the address into the search input, a click on the first autocomplete suggestion (or a submit of the box), then an evaluate returning location.href — the redirected URL contains the propertyId. Then go back to the direct gis path with {propertyId}. Many callers can avoid this by accepting a ZIP code instead (region_type=2&region_id={zip}) and lat/lon for distance-ranking.
  2. Pick the recency window. Map the caller's sale_recency to sold_within_days:

    Caller valuesold_within_days
    Last 1 month30
    Last 3 months90
    Last 6 months180
    Last 1 year365
    Last 2 years730
    Last 3 years1095
    explicit sold_after=YYYY-MM-DDcompute days delta from today, round up to nearest preset (Redfin only honors discrete buckets)
  3. Pick the search area.

    • Region-by-id (preferred when the subject's city or ZIP is known): region_type=2&region_id={ZIP} for ZIP-scoped, or region_type=6&region_id={cityId} for city-scoped. Use ZIP for tight comp sets (typical appraisal radius); use city when the ZIP is sparse.
    • Bounded radius / lat-lon recentered: pass poly={lon1}+{lat1},{lon2}+{lat2},{lon3}+{lat3},{lon4}+{lat4},{lon1}+{lat1} (the rectangle must close — first vertex repeated; + between lon and lat, %2C between vertices). Compute the rectangle as subjectLat ± (miles/69) and subjectLon ± (miles / (69·cos(lat))). poly requires market=<metro> to be supplied or it silently returns the entire metro's homes.
    • Map bounds passed by caller: drop straight into poly= in the same lon+lat order.
  4. Build the request to https://www.redfin.com/stingray/api/gis?.... Required scaffolding params: al=1&v=8&start=0&page_number=1&num_homes={limit}&include_nearby_homes=true&market={metro}&mpt=13&uipt={uipt-csv}&status=9. Then layer each filter onto the query string:

    Caller filterQuery param(s)Notes
    Sale recencysold_within_days={N}Required to switch the endpoint into sold-comps mode. Without it, the same URL returns ACTIVE listings — status alone is not enough.
    Distance / radiuspoly=... rectangle (see step 3) or region_type=2&region_id={ZIP} for "same ZIP"Redfin does not expose a pure-radius param; emulate via bounding box.
    Beds (min)num_beds={N}NOT min_beds. Aliases silently dropped.
    Beds (max)max_num_beds={N}
    Beds (exact)num_beds=N&max_num_beds=N
    Baths (min, half-bath OK)num_baths={N.0|N.5}Half-bath increments work: num_baths=2.5.
    Baths (max)max_num_baths={N}
    Price (min)min_price={N}Raw dollars (no $/k/M suffix).
    Price (max)max_price={N}
    SqFt (min)min_sqft={N} (plus min_listing_approx_size={N} — Redfin's filter page sends both)
    SqFt (max)max_sqft={N} (plus max_listing_approx_size={N})
    Lot size (min, sqft)min_parcel_size={sqft}1 acre = 43560 sqft.
    Lot size (max, sqft)max_parcel_size={sqft}
    Year built (min)min_year_built={YYYY}
    Year built (max)max_year_built={YYYY}
    Property typeuipt= CSV of 1,2,3,4,5,7,81=Single-Family, 2=Condo, 3=Townhouse, 4=Multi-Family, 5=Land, 7=Mobile/Manufactured, 8=Co-op. Combine freely.
    Days-on-market maxtime_on_market_range={N}- (note trailing dash = "N or fewer")
    Stories (min/max)min_stories={N}&max_stories={N}Optional — also requires sf=1,2,3,5,6,7 (search-feature mask).
    HOA maxNo URL param. Filter client-side on home.hoa.value after fetch.Redfin's filter UI redirects when max-hoa-fee=… is passed → confirmed unsupported on the sold-comps endpoint.
    Sold-above/below/at listNo URL param. Filter client-side: fetch each home's property-page JSON-LD to get list price, then compute delta.Redfin's UI does not expose this as a query filter for the sold view.
    Has-photos toggleNo URL param. Filter client-side on home.numPictures > 0.Redfin's URL filter has-photos redirects away → confirmed unsupported.
    Sort: most-recent saleord=redfin-recommended-asc (default; "Recently Sold" page implicitly sorts by sale recency in display order)Redfin does NOT expose a sort=newest/sold-date-desc URL key. ord=last-sale-date-desc is parsed but does NOT actually reorder — the response is identical to recommended order. To get most-recent sales first, sort client-side on home.soldDate after fetch.
    Sort: price ↓ord=price-desc&sf=1,2,3,5,6,7
    Sort: price ↑ord=price-asc&sf=1,2,3,5,6,7
    Sort: $/sqft ↑/↓ord=dollars-per-sq-ft-asc|-desc&sf=1,2,3,5,6,7
    Sort: sqft ↑/↓ord=square-footage-asc|-desc&sf=1,2,3,5,6,7
    Sort: closest to subjectord=distance-ascOnly meaningful when poly= is set; otherwise distance is from region centroid.
    Limit / page sizenum_homes={1..350}Hard cap 350 per page.
    Paginationpage_number={N}&start={(N-1)*num_homes}Both required; otherwise Redfin returns page 1.
  5. Fetch & decode: one browserless_agent call navigates straight to the gis URL and reads the JSON body in-page (a browser navigating to the endpoint renders the raw JSON as page text):

    {
      "proxy": { "proxy": "residential" },
      "commands": [
        {
          "method": "goto",
          "params": {
            "url": "https://www.redfin.com/stingray/api/gis?<query>",
            "waitUntil": "load",
            "timeout": 45000
          }
        },
        {
          "method": "evaluate",
          "params": {
            "content": "(()=>{ let t = document.body.innerText; if (t.startsWith('{}&&')) t = t.slice(4); const p = JSON.parse(t).payload; return JSON.stringify({ homes: (p.originalHomes&&p.originalHomes.homes)||[], nearby: (p.nearbyHomes&&p.nearbyHomes.homes)||[], nearbyDist: p.nearbyHomeDistance, median: p.originalHomes&&p.originalHomes.searchMedian }); })()"
          }
        }
      ]
    }

    The response body starts with the XSSI prefix {}&&strip the first 4 bytes in-page before JSON.parse (the evaluate above does this). The residential proxy is mandatory; each call is an ephemeral session. From the parsed payload read:

    • payload.originalHomes.homes[] — primary results (homes inside the requested region/poly).
    • payload.nearbyHomes.homes[] — expanded radius (populated when include_nearby_homes=true and the inner result set is sparse).
    • payload.nearbyHomeDistance — miles of the expanded ring (typically 1.0).
    • payload.originalHomes.searchMedian — region medians: {price, sqFt, pricePerSqFt, beds, baths}.
    • payload.originalHomes.gisHomesQueryId — handle for the request (helpful for caching/replay).
    • Region-wide total count is NOT returned on this endpoint. The "X homes sold matching your criteria" header on the web UI is computed client-side from the returned homes.length when < num_homes was requested. To get an exact total, request num_homes=350 and check if a 2nd page returns more; iterate until empty. (gis-aggregates exists but its payload is empty for sold queries — confirmed.)
  6. Decode each home. Every homes[i] follows the same shape:

    • Identity: propertyId (Redfin's stable ID), listingId (per-listing), mlsId.value (MLS#), url (relative path; prepend https://www.redfin.com).
    • Location: streetLine.value, unitNumber.value, city, state, zip, postalCode.value, latLong.value.{latitude,longitude}, countryCode.
    • Sold price + date: price.value (USD raw int — this is the SOLD price for sold-status homes), pricePerSqFt.value, soldDate (ms epoch — divide by 1000 for seconds, format as ISO). mlsStatus is "Sold" | "Closed" | "Closed Sale"; searchStatus=4 means sold.
    • Specs: beds, baths (decimal — 2.5 = 2 full + 1 half), fullBaths, sqFt.value, lotSize.value (sqft), yearBuilt.value, stories, propertyType (raw MLS code — varies by source), uiPropertyType (Redfin's normalized bucket: 1=SFR, 2=Condo, 3=Townhouse, 4=Multi-Family, 5=Land, 6/7=Mfd, 8=Co-op).
    • HOA: hoa.value (monthly $; absent or {level:1} with no value ⇒ no HOA or undisclosed). isHoaFrequencyKnown.
    • Photos: photos.value is a compact spec like "0-36:2" meaning photos 0–36, format spec :2. numPictures is the count. photoFormat is "webp" or "jpg". Construct the primary photo URL as: https://ssl.cdn-redfin.com/photo/{dataSourceId}/mbphoto/{last3OfListingId}/genMid.{mlsId}_0.jpg (variants: bigphoto, mbpaddedwide, bcsphoto — bigphoto is highest res). additionalPhotosInfo[] is usually empty in the list view — to get every photo URL, fetch the property page and parse <img> srcs.
    • Brokerage: sellingBroker.name, sellingAgent.name, sellingAgent.redfinAgentId, sellingBroker.isRedfin. (For sold-on-Redfin listings only; absent for off-MLS or stale records.)
    • Days on market: dom.value (when level≥2 — gated by access level). Empty dom: {level: 1} means Redfin hasn't surfaced it to the caller; fall back to the property page.
    • Misc: timeOnRedfin, timeZone, listingTags[] (highlight bullets), listingRemarks (MLS description), sashes[] (UI badges; sashTypeName=="Bought" confirms a sold-with-Redfin transaction with lastSaleDate populated).
  7. List-price-vs-sale delta (when caller requests it) — NOT in the gis JSON. For each home you want this for, open https://www.redfin.com/{home.url} with a residential-proxy browserless_agent goto + evaluate, extract offers.price from the JSON-LD block — that's the closing/list price displayed on the detail page. The MLS public-record sometimes shows originalListPrice separately in the property's "Sale & Tax History" table (further down the HTML — parse a row labelled "Listed"); subtract from price.value for the delta. Each property-page load costs roughly the same as one gis call — only do this when truly needed (e.g. when the caller filtered on sold_above_list/sold_below_list/sold_at_list).

  8. Paginate when homes.length === num_homes (likely more results). Bump page_number and recompute start=(page-1)*num_homes. Stop when a page returns fewer than num_homes.

  9. Subject framing. Attach to the response: {address, latLong, redfinEstimate, lastSale: {price, date}, url} extracted from step 1. If the caller passed lat/lon and you used a poly= search (no propertyId), set subject.address = null and report {lat, lon, radius_miles} only.

  10. Read-only enforcement. Do NOT call any /stingray/do/* POST endpoint (Save, Tour, Schedule, Submit Offer). All required data is GET-only. If the caller asks for "comps for [property] + book a tour", refuse the second clause.

Browser fallback

Only when the direct gis-navigation path fails (sustained 403s across the residential proxy pool, or you must resolve a raw street address). Drive it with a residential-proxy browserless_agent — keep the whole flow in ONE call's commands array (it's an ephemeral session, no release step):

  1. Resolve a raw street address to a propertyId — one browserless_agent call, all steps in its commands array:
    {
      "proxy": { "proxy": "residential" },
      "commands": [
        {
          "method": "goto",
          "params": {
            "url": "https://www.redfin.com/",
            "waitUntil": "load",
            "timeout": 45000
          }
        },
        {
          "method": "type",
          "params": {
            "selector": "input[name=searchInputBox]",
            "text": "{street address}"
          }
        },
        {
          "method": "waitForSelector",
          "params": {
            "selector": "[data-rf-test-name=expanded-row]",
            "timeout": 10000
          }
        },
        {
          "method": "click",
          "params": { "selector": "[data-rf-test-name=expanded-row]" }
        },
        { "method": "waitForTimeout", "params": { "time": 2500 } },
        {
          "method": "evaluate",
          "params": { "content": "(()=>location.href)()" }
        }
      ]
    }
    The search input selector is input[name=searchInputBox] (fall back to the visible search box; confirm via a snapshot command if it misses). The final evaluate returns the redirected URL, whose trailing /home/{id} segment is the propertyId; then switch back to the direct gis path with {propertyId}.
  2. Read gis via the rendered filter page — a browserless_agent goto to https://www.redfin.com/city/{regionId}/{ST}/{City}/filter/property-type=house,min-beds=3,max-beds=4,min-baths=2,min-price=500k,max-price=2M,include=sold-3mo (as the goto url), then an evaluate to pull the hydrated results out of the page's inline state (or read the property cards directly). The gis request the page fires is the same endpoint as the direct path — once you can reconstruct its URL, switch back to the cheaper direct-navigation gis call.

Site-Specific Gotchas

  • XSSI prefix is mandatory to strip: every /stingray/api/* JSON response is prefixed with the literal bytes {}&& — strip the first 4 bytes before JSON.parse. Forgetting this is the #1 cause of "the API returned garbage."
  • status=9 alone does NOT restrict to sold homes — even though Redfin's recently-sold UI URL shows status=9, the operative param is sold_within_days={N}. Without it, the same URL returns ACTIVE listings. status becomes a no-op once sold_within_days is set; you can pass status=1 or omit it and get the same sold results. Verified during iteration: status=1/2/4/8/16 with sold_within_days=180 all returned identical sold-only result sets.
  • market=<metro> is required when using poly=. Without it, poly is silently ignored and you get whole-metro homes. The market enum is the lowercase metro slug: sanfrancisco, losangeles, seattle, chicago, newyork, boston, dc, dallasfortworth, houston, phoenix, atlanta, miami, portland, denver, philadelphia, etc. You can read it from the property page JSON-LD or from the breadcrumb URL (city URL contains the metro). When you only have a region_id, omit poly entirely and the region-based scoping works without market strictness.
  • Param name aliases silently dropped: Redfin's filter page sends num_beds/max_num_beds/num_baths/max_num_baths/min_parcel_size/max_parcel_size/min_year_built/max_year_built/min_listing_approx_size/max_listing_approx_size. The "natural" aliases (min_beds, max_beds, min_lot_size, min_baths, min_sqft) are accepted by the server with HTTP 200 but silently ignored — you get unfiltered results. Always use the canonical names enumerated in step 4.
  • min_sqft AND min_listing_approx_size are both sent by the official UI for the same min-sqft filter. Either alone works; the UI passes both for redundancy. Same for max_sqft/max_listing_approx_size.
  • HOA, has-photos, % over/under list, pending toggle are NOT URL filters on the sold endpoint. Redfin's filter page redirects these specs back to a bare URL. To honor these dimensions, filter the result set client-side after fetching.
  • Sort key last-sale-date-desc is parsed but a no-op. The response is identical to redfin-recommended-asc. Same for sold-date-desc (rejected as "Invalid arguments") and closest (rejected). To deliver "Most recent sale" sort, sort the returned homes client-side on soldDate desc.
  • num_homes caps at 350 per page. Higher values are accepted but the response still returns ≤350.
  • Pagination requires BOTH page_number and start: page_number=2&start=350 (for num_homes=350). Passing only page_number returns page 1.
  • region_type enum: 1=State, 2=ZIP code, 5=County, 6=City, 12=School district. For comps you almost always want 2 (ZIP, tight) or 6 (city, broad). For a ZIP, region_id is the 5-digit ZIP itself (e.g. region_type=2&region_id=94110); for a city, look up the numeric Redfin city ID from the breadcrumb URL — there is no public city-id resolver beyond the breadcrumb.
  • mpt (map-page-type) matters: mpt=13 = sale-search-map (homepage map), mpt=99 = filter-page-map (used by /city/.../filter/... pages). Either works for the gis endpoint, but mpt must be present (omitting it returns 0 homes in some configurations). Default to mpt=13.
  • uipt (UI Property Type) is the correct property-type filter — NOT propertyType. propertyType is the raw MLS source code (varies wildly by MLS), uiPropertyType is Redfin's normalized 1–8 enum. The URL filter is uipt=.
  • Autocomplete is 403 cookieless: https://www.redfin.com/stingray/do/location-autocomplete?location=... returns 403 to an unauthenticated (no-cookie) request even through a residential proxy. To resolve a raw street address to a propertyId, the only working path is a full browser session that goes through the search box (see Browser fallback). Always prefer accepting {propertyId, ZIP, or lat+lon} from the caller and skipping address resolution.
  • belowTheFold is 403 cookieless: https://www.redfin.com/stingray/api/home/details/belowTheFold?... returns 403. Subject-property enrichment beyond JSON-LD requires the rendered property page HTML (parse id="redfin-estimate" div for the Redfin Estimate, parse the "Sale & Tax History" table for prior list/sold prices).
  • soldDate is in milliseconds-since-epoch (not seconds). Divide by 1000 before passing to datetime.fromtimestamp. The value is the closing date (deed-of-trust filing), not the offer-accepted date.
  • mlsStatus values for sold listings vary by MLS: "Sold" | "Closed" | "Closed Sale". Treat all three as "sold." Use searchStatus === 4 for a clean integer check.
  • baths is decimal, fullBaths is an int, but Redfin does not expose halfBaths directly — compute as Math.round((baths - fullBaths) * 2).
  • dom (days on market) is access-gated. For sold homes it's often surfaced only at level≥2 (Redfin sign-in). The cookieless gis path frequently returns {level: 1} (no value). When you need DOM, open the property page and parse the "Sale & Tax History" Days on Market field, OR compute soldDate - listDate from the same table.
  • Photo URL pattern by dataSourceId: dataSourceId 8 (MLSListings/Bay Area) → bigphoto and mbpaddedwide directories with ML{mlsId}_{N}.jpg. dataSourceId 10 (BAREIS) → mbphoto/bcsphoto with genMid.{mlsId}_{N}.jpg. Don't hardcode the directory — extract from a sample URL on the property page and use the discovered pattern. numPictures tells you the upper bound on _{N}.
  • listingRemarks access-gated: a cookieless gis request may return a truncated description with remarksAccessLevel: 1. Full remarks need the property page.
  • include_nearby_homes=true expands the result set when the inner region/poly has few hits — nearbyHomes.homes[] is populated with comps from a wider 1-mile ring. Set include_nearby_homes=false if the caller wants a strict in-region cut.
  • Rate limit: Redfin's edge throttles non-residential IPs. Even with a residential proxy, keep sustained throughput ≤1 req/sec. Bursts of 5+ same-IP gis requests in <1s reliably trigger 403/captcha. Each browserless_agent call is a fresh ephemeral session that draws a new residential IP when proxy: { proxy: "residential" } is set — single-shot queries are reliable; sustained scraping is not.
  • poly rectangle format: lon1+lat1,lon2+lat2,lon3+lat3,lon4+lat4,lon1+lat1 with lon and lat space-separated within each vertex (URL-encoded space = +), and %2C between vertices. The polygon must close (first vertex repeated). Order is longitude first, then latitude — reversing them returns 0 homes silently. Without market= the param is silently dropped.
  • Region-wide total count is not on the endpoint. payload.numHomesOnServer, totalUnclusteredHomes, and originalHomesCount are all null in the sold-comps response shape. To emit "X homes matching" you must paginate until exhaustion and sum, or accept "≥N" semantics.
  • gis-aggregates returns empty payload: {} for sold-status queries — don't waste a request on it.
  • Sale-to-list delta and "% over/under list" are not URL-filterable. If the caller filters on sold_above_list/sold_below_list/sold_at_list, fetch the raw set (no list/sale filter), then for each home pull its property page JSON-LD offers.price (list/last price) and compute delta = soldPrice - listPrice; partition client-side. Cost-of-each-extra-property-page is ~1 fetch per comp.
  • Redfin Estimate is not in any JSON endpoint cookieless — it's only rendered into the property page HTML as <div class="price smallerFont">$X,XXX,XXX</div> inside id="redfin-estimate". Regex out the dollar string.
  • MLS rules on photo display: some MLS sources require sign-in to surface listing photos. Cookieless Fetch may return photos.value populated but the actual CDN URL returns 403 or a placeholder. Check the response when fetching the photo URL itself; fall back to the property-page <img> srcs if needed.

Expected Output

The skill returns a single JSON object framing the subject and the comp set. Distinct outcome shapes:

// 1. Success — propertyId-driven, region-scoped, with comps
{
  "success": true,
  "subject": {
    "input_kind": "redfin_url",                       // "redfin_url" | "property_id" | "address" | "latlon"
    "property_id": 1668106,
    "url": "https://www.redfin.com/CA/Milpitas/1966-Yosemite-Dr-95035/home/1668106",
    "address": {
      "street": "1966 Yosemite Dr",
      "unit": null,
      "city": "Milpitas",
      "state": "CA",
      "zip": "95035",
      "country": "US"
    },
    "lat": 37.4302812,
    "lon": -121.8691767,
    "beds": 4,
    "baths": 2.5,
    "sqft": 2030,
    "year_built": null,
    "property_type": "Single-Family",
    "redfin_estimate_usd": 1919023,
    "last_sale": { "price_usd": 520000, "date": "2000-06-22" }
  },
  "filters_applied": {
    "sold_within_days": 90,
    "region_type": 6,
    "region_id": 12204,
    "market": "sanfrancisco",
    "uipt": [1],
    "min_price_usd": null,
    "max_price_usd": 2000000,
    "min_beds": 4,
    "max_beds": 4,
    "min_baths": null,
    "min_sqft": null,
    "max_sqft": null,
    "min_lot_sqft": null,
    "max_lot_sqft": null,
    "min_year_built": null,
    "max_year_built": null,
    "time_on_market_max_days": null,
    "sort": "redfin-recommended-asc",
    "limit": 20,
    "page_number": 1
  },
  "total_returned": 10,
  "page_size": 20,
  "more_pages_available": false,
  "region_median": {
    "sold_price_usd": 1357934,
    "sqft": 1568,
    "price_per_sqft_usd": 886,
    "beds": 3,
    "baths": 2.5
  },
  "nearby_ring_miles": 1.0,
  "comps": [
    {
      "property_id": 551401,
      "listing_id": 212753137,
      "mls_number": "ML82037320",
      "url": "https://www.redfin.com/CA/Milpitas/390-Valmy-St-95035/home/551401",
      "address": {
        "street": "390 Valmy St",
        "unit": null,
        "city": "Milpitas",
        "state": "CA",
        "zip": "95035",
        "country": "US"
      },
      "lat": 37.4550111,
      "lon": -121.9032729,
      "distance_miles": 1.69,
      "sold_price": { "formatted": "$1,500,000", "raw": 1500000, "currency": "USD" },
      "sold_date": "2026-03-24",
      "list_price_usd": null,                         // populated only if step 7 was run
      "list_to_sale_delta_usd": null,
      "list_to_sale_pct": null,
      "days_on_market": null,                          // null when access-gated; fetch property page to backfill
      "beds": 3,
      "baths": 2.0,
      "full_baths": 2,
      "half_baths": 0,
      "interior_sqft": 1100,
      "lot_sqft": 6396,
      "lot_acres": 0.147,
      "year_built": 1958,
      "stories": 1.0,
      "property_type": "Single-Family",
      "ui_property_type_id": 1,
      "hoa_monthly_usd": null,
      "price_per_sqft_usd": 1364,
      "primary_photo_url": "https://ssl.cdn-redfin.com/photo/8/bigphoto/137/ML82037320_0.jpg",
      "photo_count": 37,
      "additional_photo_urls": [],
      "selling_broker": "Redfin",
      "selling_agent": "Karan Kandel",
      "selling_broker_is_redfin": true,
      "mls_status": "Sold",
      "listing_tags": ["MODERN OPEN LAYOUT", "DESIGNER CABINETS", "WATERFALL ISLAND"]
    }
    // ...up to `limit` more comps
  ]
}

// 2. Success — lat/lon driven, poly-rectangle scoped, no propertyId
{
  "success": true,
  "subject": {
    "input_kind": "latlon",
    "property_id": null,
    "url": null,
    "address": null,
    "lat": 37.4302812,
    "lon": -121.8691767,
    "radius_miles": 0.5,
    "redfin_estimate_usd": null,
    "last_sale": null
  },
  "filters_applied": { /* same shape; region_id/region_type null, poly populated */ },
  "comps": [ /* ... */ ]
}

// 3. Empty result set — no homes matched
{ "success": true, "subject": { /*...*/ }, "filters_applied": {/*...*/}, "total_returned": 0, "comps": [] }

// 4. Address resolution failed (raw street address + cookieless gis path)
{ "success": false, "reason": "address_resolution_unavailable",
  "detail": "Redfin's autocomplete endpoint is cookie-walled (403 to an unauthenticated request). Pass a Redfin URL, propertyId, ZIP, or lat/lon, or fall back to a residential-proxy browser session through the search box.", "input": "..." }

// 5. Anti-bot wall (sustained 403 across proxy pool)
{ "success": false, "reason": "rate_limited",
  "detail": "Redfin returned 403 across N retries with rotating residential proxies. Throttle to ≤1 req/sec, or switch to the browser-fallback path.", "retries": 3 }

// 6. Invalid input (region not found, malformed ID)
{ "success": false, "reason": "subject_not_found", "detail": "...", "input": "..." }

Call it

GET https://production-sfo.browserless.io/skills?token=TOKEN-HERE&domain=redfin.com&task=get-comparable-sales-lzph4d