Zillow Leads & Property Data: Agent, Price & Tax History
Pricing
from $0.70 / 1,000 listing-rows
Zillow Leads & Property Data: Agent, Price & Tax History
Zillow listings enriched with agent/broker contact info, full price-history timeline, 20yr+ tax history, foreclosure/distress flags, schools, and resoFacts (heating/cooling/basement/construction) at scale. Pay-per-event pricing, no tiers.
Pricing
from $0.70 / 1,000 listing-rows
Rating
0.0
(0)
Developer
matata_cK
Maintained by CommunityActor stats
0
Bookmarked
1
Total users
0
Monthly active users
2 days ago
Last modified
Categories
Share
Zillow Leads & Property Data
Zillow listings enriched with agent/broker contact info, full price-history timeline, 20yr+ tax history, foreclosure/distress flags, schools, and resoFacts (heating/cooling/basement/construction) at scale. No other Zillow actor extracts these at this depth.
TL;DR
- Bare listings arrive fast (address, price, beds/baths, status): this data comes straight off Zillow's search results.
- Enriched rows (agent contact, price/tax history, foreclosure, schools, resoFacts) take longer. See "Why enrichment is slower than listings" below before assuming something's stuck.
- Dedup is built in: upload
dedupZpids/dedupMlsIdsfrom your last order and we never re-charge you for rows you already have. - No tiers, pay-per-event:
actor-startonce, then per row at the depth you actually received. - Minimums always hold (1000 listings / 500 enriched by default, applies to every mode) even if that means a short wait for the shortfall to collect.
Overview
This actor sources live Zillow listing data and, at enriched depth, joins
in the fields that make a listing actually useful to lead-gen, investors,
appraisers, and proptech: agent/broker contact, full price and tax history,
foreclosure/distress signals, schools, and the resoFacts long tail
(heating/cooling/basement/construction) that most competitors don't surface
at scale.
Every row carries detail_url, the live Zillow listing link, so you can
always verify a row against the source. Rows never carry raw scraped
HTML/JSON, only the extracted field set below.
What you get
| Segment | Fields |
|---|---|
| Lead-gen, agents, mortgage, insurance, wholesalers | Agent/broker name, phone, email, license, brokerage, MLS attribution |
| Wholesalers, flippers, hedge funds, hard-money | Foreclosure/distress type, dates, loan/past-due amounts |
| Appraisers, iBuyers, AVMs, analysts | Full price-history timeline (event/date/price/source/$-per-sqft) |
| Tax analytics, holding-cost investors, insurance | 20yr+ tax history + assessed value |
| Relocation, family search | Assigned + nearby schools |
| Title, county aggregators, proptech | Parcel number/ID, subdivision, zoning |
Choosing a mode (start here)
Every order needs exactly one mode. It's the first field in the input
form, and it determines which OTHER fields actually do anything. The Apify
Console form shows all three modes' fields at once (Apify doesn't support
hiding fields conditionally), so use this table to know which section to
actually fill in:
| Mode | Required fields | Optional fields (this mode) | Fields this mode ignores |
|---|---|---|---|
catalog | none | metro (scopes the snapshot) | bounds, depth, source |
custom_search | bounds OR metro | depth, minListings, minEnriched | source |
recent_activity | none (source defaults to agent) | source, minEnriched | bounds, metro, depth, minListings (see note) |
Fields NOT in this table (dedupZpids/dedupMlsIds, timeoutSecs) apply
the same way regardless of mode.
Whichever mode you pick, check your run's key-value store for an
ORDER_SUMMARY record once the run starts: the raw Input tab always
shows every field above regardless of mode (Apify's Console form submits
the whole schema unconditionally; it can't hide irrelevant fields), so a
catalog run's Input tab still shows a populated bounds even though
catalog ignored it entirely. ORDER_SUMMARY is the unambiguous answer:
only what actually applied, plus, once rows start arriving,
cities_included, the real city/suburb names your bounds or metro pick
actually returned (see "Custom search" below for why that's often more
than one city).
recent_activity technically accepts minListings without erroring, but
the sitemap feed is always fully enriched and doesn't distinguish "listings"
from "enriched" the way custom_search does; use minEnriched there.
- Catalog: instant snapshot from our pre-seeded showcase metros.
Fastest option; returns in seconds since it never needs to wait on live
collection at all (pulled straight from our own cache). No fields
required, just set
mode: catalog. As of this writing the catalog pool covers New York City, Los Angeles, Chicago, Houston, and Phoenix (~1000 listings each, more metros added over time).ORDER_SUMMARYstates the current coverage on every catalog run so you don't have to track it here. Optionally setmetroto scope the snapshot to just one of those metros instead of the whole pool (still instant: this only filters what's already cached, it never triggers live collection). - Custom search: supply either
metro(pick a named metro from the list, the easy option, no lat/lng math) orbounds, a lat/lng bounding box for a precise custom areametrodoesn't cover:north/southare latitude,east/westare longitude, andnorthmust always be the LARGER (more northerly) value, withsouth/westthe smaller ones. A swapped box (e.g.northgiven as the smaller number) is rejected immediately with an explanatory error rather than silently returning nothing, and so is an order with neithermetronorboundsset. If you set both,boundswins. Either way this catches a whole metro area (adjacent suburbs included), not just one named city: e.g. a Phoenix query genuinely also returns Scottsdale, Tempe, Glendale, and other real suburbs, confirmed against our own collected data; checkORDER_SUMMARY'scities_includedon your run to see exactly which ones landed in yours. The only mode wheredepth: listings(see "Bare vs enriched schema" below) is available as a cheaper/faster alternative to full enrichment. - Recent activity: Zillow's sitemap feed of newest/changed listings
nationwide, no bounds needed.
source:agent(bulk of inventory) orowner(FSBO, smaller feed). Always fully enriched: the feed itself carries no fields beyond a listing URL, so there's no cheaper "bare" variant to offer here.
Custom search and recent-activity orders that aren't already covered by our
existing coverage are collected live; this can take a while (see
"Why enrichment is slower than listings" and timeoutSecs), and you'll get
a partial dataset if the timeout elapses first.
Caps, explained
Two numbers control how much you get; both are floors, not targets:
you may receive more than the minimum in one delivery (e.g. Apify's KV
transport ships in ~100-row chunks, and you'll get the whole chunk that
crosses the line), but never less unless timeoutSecs runs out first.
minListings(default and recommended value: 1000, matching our 1000-per-metro catalog seeding target): the floor for total rows, at whatever depth you asked for. Applies to every mode. It can technically be set as low as 1 (there's no hard-coded floor beyond rejecting zero/negative input); do that only for a quick test run (e.g. 10-50) before committing to a real order. 1000 is what we'd suggest for anything you actually intend to use.minEnriched(default and recommended value: 500): the floor for how many of those rows must be fully enriched. Only meaningful when enrichment is actually happening:catalog,recent_activity, orcustom_searchwithdepth: enriched. Forcustom_searchwithdepth: listings, this is forced to0internally regardless of what you pass, since a listings-depth order never enriches anything by design, so there's nothing for this number to floor. If you set both by hand,minEnrichedmust be <=minListings: asking for more enriched rows than total rows is a nonsensical order.- See the example inputs below for both a quick low-cap test and a full 1000/500 order.
timeoutSecs(default 7200 = 2 hours) is the most wall-clock time we'll spend trying to reach these floors before handing back a partial dataset. A fully cache-satisfiable order returns in seconds regardless of this value.
Bare vs enriched schema: know what fields to expect
depth: listings rows only ever include NON-NULL fields from this
whitelist: don't assume every key below is present on every row; a field
Zillow simply didn't return for that listing is omitted entirely rather than
sent as null. This is the ONLY thing a depth: listings row will ever
contain: no agent contact, no history, no resoFacts, even if we happen to
already have that listing fully enriched from an earlier order (we
deliberately strip it back down so you never pay the listings rate for
enriched-rate data):
| Group | Fields |
|---|---|
| Identity / link | zpid, mls_id, detail_url |
| Price / status | price, status_type, status_text |
| Size / type | bedrooms, bathrooms, living_area, home_type, lot_area_value, lot_area_unit |
| Location | address_street, address_city, address_state, address_zipcode, latitude, longitude |
| Timing | year_built, days_on_zillow |
| Open house | has_open_house, open_house_start, open_house_end |
| Occupancy/ownership flags | is_non_owner_occupied, is_zillow_owned, country |
| Listing-type flags | is_fsba, is_fsbo, is_bank_owned, is_coming_soon, is_for_auction, is_foreclosure, is_new_home, is_open_house, is_pending |
depth: enriched rows (the default for every mode) include everything
above, PLUS the full groups from "What you get" below: agent/broker
contact, foreclosure/distress detail, complete price-history and
20yr+ tax-history timelines, assigned + nearby schools, resoFacts
(heating/cooling/construction/parking/etc.), HOA fees, mortgage/market
timing, and the structural/legal/rural/rental long tail. Enriched rows can
still omit individual fields Zillow didn't return for that listing (e.g. a
condo with no heating/cooling data), but the row is never retroactively
stripped back down the way a listings-depth row is.
Why enrichment is slower than listings
We deliberately don't drive a full browser automation stack (no
Playwright/Puppeteer/Selenium session juggling); collection uses a lighter,
more targeted request method instead. That keeps bare listing data flowing
quickly. But Zillow runs an aggressive Web Application Firewall in front of
its detail pages, and getting flagged there risks losing collection
entirely, not just slowing it down for one order. So enrichment (agent
contact, price/tax history, foreclosure, schools, resoFacts) is paced
deliberately conservatively: correctness and continued access matter more
than raw speed. In practice: expect bare listings within seconds to minutes,
and budget real wall-clock time (timeoutSecs) for enrichment on anything
not already in our existing coverage.
Example inputs
Each block below is a complete, ready-to-run input; paste it directly into the Input JSON tab (switch from the regular form editor via the toggle above the input form) to skip filling in the form by hand.
Quick test run (small, cheap, fast, before committing to a large order):
{"mode": "custom_search","bounds": { "north": 40.75, "east": -73.7, "south": 40.7, "west": -73.78 },"depth": "listings","minListings": 10,"timeoutSecs": 300}
Catalog (instant snapshot, no fields to set):
{ "mode": "catalog" }
Catalog, scoped to one metro (still instant, filters the cache, no live collection):
{ "mode": "catalog", "metro": "Chicago" }
Custom search by metro name (no lat/lng math needed):
{"mode": "custom_search","metro": "Phoenix","depth": "enriched","minListings": 1000,"minEnriched": 500}
Custom search, bare listings only (cheaper, faster):
{"mode": "custom_search","bounds": { "north": 40.75, "east": -73.7, "south": 40.7, "west": -73.78 },"depth": "listings","minListings": 1000}
Custom search, fully enriched:
{"mode": "custom_search","bounds": { "north": 34.34, "east": -118.12, "south": 33.7, "west": -118.67 },"depth": "enriched","minListings": 1000,"minEnriched": 500}
Recent activity, newest/changed agent-listed inventory:
{ "mode": "recent_activity", "source": "agent" }
Recent activity, FSBO (owner-listed) only:
{ "mode": "recent_activity", "source": "owner" }
Repeat order with dedup (never re-ship what you already have; reuse the
DEDUP_UPDATE key from your prior run's key-value store):
{"mode": "custom_search","bounds": { "north": 40.75, "east": -73.7, "south": 40.7, "west": -73.78 },"dedupZpids": [43814015, 43828670],"dedupMlsIds": ["A12046823"]}
Zero rows with no obvious reason?
A run can finish SUCCEEDED with zero rows for a legitimate reason (bad
input, mostly) rather than a real failure; we don't fail the run outright
for this, since the collection service itself worked fine. Check your run's
Key-value store tab (not the Dataset tab): a rejected order (e.g. a
malformed bounds box) writes a STATUS key there,
{"message": "<reason>"}, so you don't have to go hunting through logs or
run metadata to find out why. (We also set it as the run's status message,
but don't rely on that alone being visible once a run has finished; the
STATUS key is the reliable place to look. An agent can fetch it directly
with get-key-value-store-record.)
Dedup
Pass dedupZpids/dedupMlsIds with IDs you've already received from a
prior run and we'll never re-ship them. Every delivery that returns at
least one row writes a DEDUP_UPDATE key to that run's key-value store
({"zpids": [...], "mls_ids": [...]}), the union of what you told us you
already had plus everything this run just shipped; copy those two arrays
into dedupZpids/dedupMlsIds on your next order to keep your delivered
set current. It's in the key-value store rather than the dataset
deliberately, so it never shows up as a stray non-listing row if you're
iterating dataset items programmatically.
Pricing (pay-per-event, no tiers)
| Event | When | Price |
|---|---|---|
actor-start | once per run | $0.00001 |
listing-row | per row at listings depth | $0.0007 (~$0.70 per 1000 rows) |
enriched-row | per row at enriched depth | $0.0012 (~$1.20 per 1000 rows) |
Operational note
If collection is briefly interrupted, your order queues and resumes automatically, no action needed on your end. If it's taking longer than expected, that's the WAF-safe pacing described above at work, not a stall.
Use from an AI agent (MCP)
This Actor is callable as a tool by AI agents via Apify's actors-mcp-server. Once the server is wired into an MCP-capable client (Claude Desktop, Cursor, ZCode, OpenCode, etc.), the agent sees this Actor as a tool whose input schema is the form above and can invoke it mid-task, no manual input-form filling.
Two connection modes:
- Local stdio (
npx @apify/actors-mcp-server): exposes any Actor your Apify API token can run, including this one before it's Store-published. Filter the server to just this Actor with--tools germane_binoculars/zillow-leads-property-data. This is how to try it against your own account today. - Hosted remote (
https://mcp.apify.com): discovers Actors from the Apify Store, so this mode applies once the Actor is published. Per Apify's own docs, this path also supports output schema inference (field-level types) that the local stdio path doesn't. Either way, this Actor ships its own explicit dataset schema (.actor/output_schema.json, 229 fields), so an agent can know the exact result shape up front regardless of which connection mode it uses.
The input fields above are written to be self-documenting for an agent:
each carries a plain-language description of when it applies and how the
value is interpreted, so an LLM can construct a valid call without reading
this README first. catalog mode (the default) is the fastest agent smoke
test: it returns cached rows in seconds with no bounding box or live
collection, and no polling required, so an agent can confirm the
integration works end-to-end in one round-trip.
Long-running orders: fire, poll, fetch
A live-collect order (custom_search or recent_activity) takes real
wall-clock time (minutes, not seconds) because enrichment (agent contact,
price/tax history) is paced for WAF safety. A single MCP tool call won't
block long enough to see it through, and that's by design. The natural agent
interaction is three steps:
-
Fire: call the Actor tool with your input + a generous
timeoutSecs. Sustained enrichment throughput is rate-limited for site-safety reasons, so a default-sized order (minListings: 1000, enriched) realistically takes 90-190 real minutes, not the 600s a smaller test order might suggest. 600s+ is only "generous" for a small test run (); for anything near the 1000-row default, budget close to the full 7200s default rather than overriding it lower. The tool blocks up to 45s (its ownminListings: 10-50waitSecscap, separate fromtimeoutSecs), then if the run is still going it returns a status line like "RUNNING for 40s. 2 results so far" plus anextSteptelling you exactly how to poll. Don't treat this as an error; the run is healthy and collecting. -
Poll: follow
nextStepby callingget-actor-runwith therunIdandwaitSecs: 30repeatedly until the status readsSUCCEEDED(orTIMED-OUTwith partial results). Each poll shows incremental progress (the result count climbs), so you can watch collection advancing. -
Fetch: once terminal, call
get-dataset-itemswith thedatasetIdfrom the response to retrieve the actual rows.
timeoutSecs (your input) governs how long collection is allowed to run
before the run returns a partial dataset; set it to comfortably exceed the
worst case for your order size. It is NOT the same as waitSecs (the MCP
tool's single-call block cap of 45s): the two are independent, and a
correct agent flow respects both.