Building a Comps Report: Zillow Recently Sold Data for Accurate CMAs
How to pull recently-sold Zillow comps by ZIP, filter by beds and square footage, and calculate price-per-sqft for a defensible comparative market analysis.
The actor referenced in this article. Pay only for results delivered.
A comparative market analysis is only as good as the comps behind it. Pull the wrong set — too few sales, too wide a date window, mismatched square footage — and the valuation drifts. Pull a tight, recent, well-matched set and the number holds up to scrutiny from a buyer, a lender, or an investor’s own model.
The hard part has never been the math. Price per square foot on five recent sales is a spreadsheet formula. The hard part is getting the sales data itself. Zillow’s public site shows recently-sold homes for any ZIP, but there is no supported way to pull that list programmatically — the old Zillow API (GetDeepSearchResults, GetUpdatedPropertyDetails) was shut down years ago, and current partner APIs are gated to lenders and portals, not to someone running comps for a single deal.
The Zillow Recently Sold Scraper fills that gap. It queries Zillow’s own search backend for sold homes in a location and date window, and returns sold price, sold date, beds, baths, square footage, lot size, Zestimate, and coordinates — the fields a CMA actually needs — with no login and no API key.
What a CMA needs from a comps set
A defensible CMA needs comps that are:
- Recent — sold within a window that reflects the current market, not a snapshot from a year ago
- Geographically close — same ZIP or a tight radius, not “somewhere in the metro”
- Similar in size — within a reasonable band of the subject property’s square footage and bed count
- Enough of them — three sales is thin; eight to fifteen gives you a real distribution
The Zillow Recently Sold Scraper’s input maps directly onto those requirements. location sets the geography (a city, or a ZIP code like 78704), soldInLast sets the recency window (30, 90, 180, or 365 days), and minBeds, minPrice, maxPrice, and propertyType narrow the size and type filters.
Pulling comps in Python
import os
from apify_client import ApifyClient
apify = ApifyClient(os.environ["APIFY_TOKEN"])
run = apify.actor("themineworks/zillow-recently-sold").call(run_input={
"location": "78704",
"maxItems": 100,
"soldInLast": 90,
"minBeds": 3,
"propertyType": "SINGLE_FAMILY",
})
comps = [item for item in apify.dataset(run["defaultDatasetId"]).iterate_items()]
print(f"Pulled {len(comps)} sold comps in the last 90 days")
Each record comes back as a flat JSON object — soldPrice, soldDate, sqft, beds, baths, zestimate, latitude, longitude, and the listing’s detailUrl and zpid. No parsing HTML, no dealing with Zillow’s client-rendered search UI.
Filtering to a tight comp set
Pulling 90 days of sold homes in a ZIP often returns more variety than you want — a mix of condos, fixer-uppers, and larger homes alongside properties that actually match your subject. Filter down in Python before calculating anything:
subject_sqft = 1720
tolerance = 0.20 # +/- 20%
tight_comps = [
c for c in comps
if c.get("sqft") and c.get("soldPrice")
and abs(c["sqft"] - subject_sqft) / subject_sqft <= tolerance
]
print(f"{len(tight_comps)} comps within {int(tolerance*100)}% of subject sqft")
Calculating price per square foot
With a tight comp set, the core CMA number is straightforward:
price_per_sqft = [c["soldPrice"] / c["sqft"] for c in tight_comps]
avg_ppsf = sum(price_per_sqft) / len(price_per_sqft)
estimated_value = avg_ppsf * subject_sqft
print(f"Average $/sqft across {len(tight_comps)} comps: ${avg_ppsf:,.2f}")
print(f"Estimated value for {subject_sqft} sqft subject: ${estimated_value:,.0f}")
For a more resilient estimate, use the median instead of the mean — a single outlier sale (a distressed sale, or a renovated flip) can skew an average of five to eight comps more than you’d like.
import statistics
median_ppsf = statistics.median(price_per_sqft)
print(f"Median $/sqft: ${median_ppsf:,.2f}")
Cross-checking against Zestimate
Every sold record includes Zillow’s own zestimate field where Zillow published one at the time of the listing. It is worth pulling as a sanity check — not as the valuation itself, since Zestimates are Zillow’s automated model and diverge from actual sale price by varying amounts depending on the market, but as a second signal that your comps-based number is in a reasonable range.
zestimates = [c["zestimate"] for c in tight_comps if c.get("zestimate")]
if zestimates:
avg_zestimate_vs_sold = statistics.mean(
c["zestimate"] / c["soldPrice"] for c in tight_comps
if c.get("zestimate") and c.get("soldPrice")
)
print(f"Zestimate averaged {avg_zestimate_vs_sold:.2%} of actual sold price in this comp set")
That ratio — Zestimate versus actual sold price — is itself useful. If Zestimates in a ZIP are running 8% high relative to sold price, that tells you something about how much to discount Zillow’s own valuation for that specific market.
Running it as a scheduled pull
A CMA is often a one-off exercise for a single deal, but for an agent or investor running comps regularly, scheduling the actor on Apify keeps a comps dataset current. Point a weekly or monthly schedule at the ZIP codes you track, and each run appends fresh sold data without you re-triggering it manually.
Wiring it into an MCP agent
For a faster, more conversational workflow — pulling comps for a one-off address without writing a script — the actor is available as an MCP server:
https://mcp.apify.com/?tools=themineworks/zillow-recently-sold
Connected to Claude or any MCP client, you can ask “pull recently-sold 3-bed homes in 78704 from the last 90 days and estimate price per square foot” and get comps back in the conversation.
Where this fits in a larger data stack
The Recently Sold Scraper answers one question: what actually sold, and for how much. It pairs naturally with the Zillow Search Scraper for active for-sale inventory (to compare asking price against recent sold comps), and with the Zillow Property Details Scraper when you need full price history, tax history, and school data for a specific subject property rather than just the comp set.
For a walkthrough of building the Zillow side of a data stack end to end, see Building a Real Estate Data Stack: Zillow, Redfin, and Realtor.com. For the Python fundamentals of working with Zillow’s search data more broadly, see Zillow Scraper in Python: A 2026 Guide.
Pricing
Results cost $1 per 1,000 ($0.001 each), with nothing charged on empty or failed pulls. Pulling 90 days of sold comps for a busy ZIP — typically 50-150 records — costs a few cents. Running the same pull monthly across a dozen ZIP codes a real estate team tracks costs a few dollars a month, well below what a comps subscription service charges per seat.
Frequently Asked Questions
How far back can I pull sold comps?
The soldInLast parameter accepts 30, 90, 180, or 365 days. For an active market, 90 days usually gives a large enough sample without going so far back that the comps no longer reflect current pricing. In slower markets, 180 days may be necessary to get enough sales.
Does this replace a licensed appraisal?
No. This is comps data for informal valuation — pricing a listing, sizing an offer, sanity-checking a lender’s number, or feeding a larger investor model. A licensed appraisal involves inspection and adjustments a scraped dataset cannot replicate.
Can I get the full sale history of a specific property, not just recent comps?
Yes, but that requires the separate Zillow Property Details Scraper, which takes a detailUrl or zpid and returns full price history, tax history, and more. The Recently Sold Scraper is built for pulling many comps across a location, not deep-diving one address.
Explore the scraper referenced in this article — see inputs, outputs, and pricing, then run it on Apify.
I Automated My AI Job Hunt: 8 Tools, One Claude Agent, Under $1
Most job hunting is spent on the wrong problem. Here is the exact end-to-end flow — find who is actually hiring, rank by real fit, research the company, find the human, and write an application grounded in facts — run entirely from Claude via MCP.
Ask an AI for a Company's SEC CIK and It Will Lie to You. Here Is the Fix.
An AI that remembers is not an AI that verifies. This is the exact diligence flow — legal identity from GLEIF, filings from EDGAR, funding, engineering activity and hiring — run from Claude via MCP, grounded in real registries, for about fifteen cents a company.
Stop Cold Pitching: Find Local Businesses With a Problem You Can Actually Fix — Under $4
Cold outreach fails because it is generic. Here is the exact flow — build a prospect universe, diagnose the real problem from their own reviews, qualify who can pay, find the human, and pitch with evidence — run entirely from Claude via MCP. No code.