How to Screen Rental Yield With Zillow Rent Zestimate Data
Rank hundreds of for-sale homes by rent Zestimate divided by price, shard cheap cash-flow markets by ZIP, and shortlist the ten worth underwriting this week.

To screen rental yield at scale, pull active for-sale listings with both Zestimate fields attached, then rank them on
rent_zestimate × 12 ÷ price. The Zillow Suite Scraper returns price, beds, baths, living area, Zestimate, Rent Zestimate, tax assessed value and days on market on every row, so the ratio is a one-line calculation. Shard a cheap metro into ZIP codes, run once, and you have a ranked buy-list instead of a browser tab graveyard.
Why screen rental yield with Zillow Rent Zestimate data
Small investors and emerging funds lose most of their acquisition time to triage, not to underwriting. A Cleveland or Memphis metro has thousands of active listings under $200,000; maybe forty of them clear a yield threshold, and maybe ten survive a real look at the roof, the neighbourhood and the tax bill. The bottleneck is getting from thousands to forty without opening thousands of tabs.
Zillow already publishes the two numbers that ratio needs on the same listing card. The list price is there, and so is the Rent Zestimate, its model-generated monthly rent. Divide one by the other and you have gross yield — a crude number, but a fast and consistent one. Applied uniformly across every listing in a market, it does exactly what a screen is supposed to do: rank things so you can ignore most of them.
This matters because the buyer side of this market is mostly individuals, not institutions. The US Census Bureau's Rental Housing Finance Survey finds that individual investors own the large majority of one-unit rental properties in the country. That is a market of operators with small teams and no data department, competing against each other on speed of triage. A ranked list on Monday morning is the whole edge.
How does this compare to the alternatives?
Screening yield needs breadth, not depth. You need every listing in a market with two fields attached, refreshed often enough to matter — which is a different problem from pulling one property in rich detail. Three approaches exist, and they differ mostly in how much of your week they consume.
| Approach | Cost | Reliability | Setup time | Maintenance |
|---|---|---|---|---|
| DIY Python scraper | Free until it isn't — your time plus proxy bills | Breaks on Zillow's fingerprinting and rate limits; partial result sets you can't detect | 2–5 days to first clean run | Constant; Zillow's payload shape moves |
| Generic scraping API | Subscription regardless of usage | Fetches the page but hands you raw HTML — you still write the Zillow parser | 1–2 days plus parser work | You own the parser forever |
| Zillow Suite Scraper | Transparent pay-per-result, no subscription | Sessions rotate and responses are validated by shape, so a challenge page is never counted as data | Under 10 minutes | None — the parsing and normalisation are maintained for you |
The practical difference is the normalisation. Prices come back as numbers, not "$129,900". Lot sizes are converted to square feet. Dates are ISO 8601. price_per_sqft is already computed. You go from run to spreadsheet without writing a cleaning layer.
How to screen rental yield with Zillow data in 4 steps
How do I pull for-sale listings with rent estimates for a target market?
Run the Actor in forSale mode with a price ceiling that matches your buy box. Every search row already carries zestimate and rent_zestimate, so no detail pass is needed for a first screen.
from apify_client import ApifyClient
client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("thirdwatch/zillow-suite-scraper").call(run_input={
"queries": ["44109", "44111", "44135"], # Cleveland cash-flow ZIPs
"searchMode": "forSale",
"maxResults": 400,
"minPrice": 60000,
"maxPrice": 220000,
"minBeds": 2,
"homeTypes": ["houses", "multiFamily"],
"sortBy": "newest",
"includeDetails": False,
})
rows = list(client.dataset(run["defaultDatasetId"]).iterate_items())
print(len(rows), "listings")includeDetails: False is deliberate. Detail mode costs one extra page load per listing and you do not need year built or the agent's phone number to rank a hundred homes. Save it for the shortlist.
How do I compute gross yield from the Zillow fields?
Multiply rent_zestimate by twelve and divide by price. Drop any row where either field is missing rather than substituting a guess, because a fabricated rent will float straight to the top of your ranking.
def gross_yield(row):
price = row.get("price")
rent = row.get("rent_zestimate")
if not price or not rent:
return None
return (rent * 12) / price
scored = []
for row in rows:
y = gross_yield(row)
if y is None:
continue
scored.append({
"zpid": row["zpid"],
"address": row["address"],
"zip_code": row["zip_code"],
"price": row["price"],
"rent_zestimate": row["rent_zestimate"],
"zestimate": row.get("zestimate"),
"gross_yield": round(y * 100, 2),
"price_per_sqft": row.get("price_per_sqft"),
"days_on_zillow": row.get("days_on_zillow"),
"url": row["property_url"],
})
scored.sort(key=lambda r: r["gross_yield"], reverse=True)
for r in scored[:10]:
print(f'{r["gross_yield"]}% ${r["price"]:,} {r["address"]}')Join the second Zestimate field while you are here. A listing where price sits well above zestimate is asking more than Zillow's own model thinks it is worth — worth flagging even when the yield looks good.
How do I cover a whole metro instead of one city?
Pass many narrow queries instead of one broad one. Zillow serves 41 listings per page and stops paginating at roughly 980 listings per search area, so "Cleveland, OH" will report tens of thousands of matches and hand over the first thousand. ZIP-level shards get past that ceiling.
CLEVELAND_ZIPS = [
"44102", "44105", "44109", "44111", "44113", "44119",
"44125", "44128", "44134", "44135", "44144",
]
run = client.actor("thirdwatch/zillow-suite-scraper").call(run_input={
"queries": CLEVELAND_ZIPS,
"searchMode": "forSale",
"maxResults": 500, # per ZIP, not per run
"maxPrice": 220000,
"homeTypes": ["houses", "multiFamily"],
})
# Rows are deduplicated on zpid within a run, so overlapping
# shards are safe. Dedupe across runs yourself:
seen, unique = set(), []
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
if row["zpid"] in seen:
continue
seen.add(row["zpid"])
unique.append(row)maxResults applies per entry in queries, so eleven ZIPs at 500 each is an eleven-shard sweep, not a 500-row run. This is also the pattern behind building a Zillow API alternative for a property app.
How do I add taxes and HOA to get a net yield?
Enrich only the top slice. Turn on includeDetails and cap it with maxDetailPages so you pay for detail on the shortlist rather than the whole market.
shortlist = [r["url"] for r in scored[:25]]
run = client.actor("thirdwatch/zillow-suite-scraper").call(run_input={
"queries": shortlist, # /homedetails/ URLs are scraped as single rows
"includeDetails": True,
"maxDetailPages": 25,
})
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
rent = (row.get("rent_zestimate") or 0) * 12
hoa = (row.get("monthly_hoa_fee") or 0) * 12
tax = row.get("annual_tax_amount") or 0
net = (rent - hoa - tax) / row["price"] if row.get("price") else None
print(row["address"], row.get("year_built"), round((net or 0) * 100, 2))Detail rows add year_built, monthly_hoa_fee, annual_tax_amount, property_tax_rate, mls_id, price_history, tax_history and assigned schools — enough to kill the obvious losers before anyone drives out.
How do I export the shortlist for underwriting?
Write the scored rows straight to CSV and hand them to whoever builds the model. The field names are stable, so the same sheet works week after week.
import csv
with open("buy_list.csv", "w", newline="") as f:
w = csv.DictWriter(f, fieldnames=list(scored[0].keys()))
w.writeheader()
w.writerows(scored[:50])Sample output
Every row is one listing, keyed on zpid — Zillow's stable property ID, and the right primary key for your own store. The two fields the screen runs on are price and rent_zestimate; zestimate is the sanity check against the ask, and days_on_zillow plus price_change tell you whether the seller is already moving.
[
{
"zpid": "33580919",
"property_url": "https://www.zillow.com/homedetails/3821-W-46th-St-Cleveland-OH-44109/33580919_zpid/",
"source_query": "44109",
"search_mode": "forSale",
"listing_status": "FOR_SALE",
"status_text": "House for sale",
"home_type": "SINGLE_FAMILY",
"address": "3821 W 46th St, Cleveland, OH 44109",
"city": "Cleveland",
"state": "OH",
"zip_code": "44109",
"latitude": 41.446312,
"longitude": -81.707841,
"price": 129900,
"currency": "USD",
"price_per_sqft": 92.13,
"bedrooms": 3,
"bathrooms": 2,
"living_area_sqft": 1410,
"lot_size_sqft": 4356,
"zestimate": 134700,
"rent_zestimate": 1425,
"tax_assessed_value": 68300,
"days_on_zillow": 22,
"price_change": -5000,
"scraped_at": "2026-07-28T04:11:20+00:00"
},
{
"zpid": "33612204",
"property_url": "https://www.zillow.com/homedetails/4410-Tillman-Ave-Cleveland-OH-44135/33612204_zpid/",
"source_query": "44135",
"search_mode": "forSale",
"listing_status": "FOR_SALE",
"home_type": "MULTI_FAMILY",
"address": "4410 Tillman Ave, Cleveland, OH 44135",
"city": "Cleveland",
"state": "OH",
"zip_code": "44135",
"price": 164500,
"currency": "USD",
"price_per_sqft": 71.52,
"bedrooms": 4,
"bathrooms": 2,
"living_area_sqft": 2300,
"zestimate": 158900,
"rent_zestimate": 2050,
"tax_assessed_value": 79100,
"days_on_zillow": 61,
"price_change": null,
"scraped_at": "2026-07-28T04:11:26+00:00"
}
]The first row screens at 13.2 percent gross and is priced below its Zestimate. The second screens at 14.9 percent but has sat for 61 days and is asking above the Zestimate — the ratio flags it, the other fields tell you why to be careful.
Common pitfalls
Not every listing carries a Rent Zestimate. New construction, off-market and agent-withheld listings routinely omit rent_zestimate, zestimate or lot size. Missing values return as null rather than being guessed, so filter before you divide.
Apartment buildings are not scoreable. Rental buildings come back as one row with is_building: true and a rent_min/rent_max range, with no Zestimate or tax value attached, because Zillow does not hold a property record for them. Exclude them from a yield screen.
One query will not cover a metro. The ~980-listing ceiling per search area is a hard stop. Shard by ZIP or neighbourhood; the Actor logs a warning when it hits the ceiling so you know a shard was truncated.
A minority of requests get challenged. Zillow rejects some sessions outright. The Actor retries on a fresh session with backoff and recovers nearly all of them, but an aggressive run can still finish a shard short of maxResults — compare row counts against your ZIP list before you conclude a market is thin.
It is US-only, and public data only. No Zillow account, saved search or agent-gated field is in scope.
Related use cases
- Pull Zillow sold comps for CMA valuation reports — once a shortlist clears the screen, price it on real sold evidence.
- Monitor Zillow price drops and new listings daily — run the same shards on a schedule and catch cuts within hours.
- Build a Zillow API alternative for a property app — the ZIP-sharding and
zpiddedupe pattern, turned into an ingestion pipeline. - Build real-estate agent lead lists from Zillow — the contact side of the same dataset.
- Guide to scraping real-estate data — the category pillar.
- More walkthroughs on the Thirdwatch blog, and the actor page at /scrapers/zillow-suite-scraper.
Frequently asked questions
What is a good gross rental yield to screen for?
Most small US investors screen for gross yields above 8 percent, which is roughly the 1 percent rule expressed annually. Below 6 percent the deal usually needs appreciation to work. Screening is triage, not underwriting: the ratio tells you what deserves a closer look.
How accurate is the Zillow Rent Zestimate?
Treat it as a directional estimate, not an appraisal. It is generated from Zillow's own model, and it is strong enough to rank a hundred homes against each other but not to set an offer price. Verify the top ten against live rental comps before you underwrite.
Can I screen an entire metro in one run?
Not with one query. Zillow stops serving new results at roughly 980 listings per search area, so pass a list of ZIP codes or neighbourhoods instead of one city name. Rows are deduplicated on zpid within a run, so overlapping areas are safe.
Do sold listings carry a Rent Zestimate too?
Yes, where Zillow publishes one. Sold rows return zestimate, rent_zestimate and tax_assessed_value alongside date_sold, which is useful for checking whether the yields you are screening today are consistent with what actually traded.
Does every listing have a rent_zestimate?
No. New construction, off-market and agent-withheld listings routinely omit it, and apartment building rows have no property record attached at all. Missing values come back as null rather than a guess, so filter them out before you compute the ratio.
Related
100 free credits, no credit card.
About 30 real searches. Add the MCP to Claude or Cursor in two minutes.