Zillow Leads & Property Data: Agent, Price & Tax History avatar

Zillow Leads & Property Data: Agent, Price & Tax History

Pricing

from $0.70 / 1,000 listing-rows

Go to Apify Store
Zillow Leads & Property Data: Agent, Price & Tax History

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

matata_cK

Maintained by Community

Actor stats

0

Bookmarked

1

Total users

0

Monthly active users

2 days ago

Last modified

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/dedupMlsIds from your last order and we never re-charge you for rows you already have.
  • No tiers, pay-per-event: actor-start once, 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

SegmentFields
Lead-gen, agents, mortgage, insurance, wholesalersAgent/broker name, phone, email, license, brokerage, MLS attribution
Wholesalers, flippers, hedge funds, hard-moneyForeclosure/distress type, dates, loan/past-due amounts
Appraisers, iBuyers, AVMs, analystsFull price-history timeline (event/date/price/source/$-per-sqft)
Tax analytics, holding-cost investors, insurance20yr+ tax history + assessed value
Relocation, family searchAssigned + nearby schools
Title, county aggregators, proptechParcel 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:

ModeRequired fieldsOptional fields (this mode)Fields this mode ignores
catalognonemetro (scopes the snapshot)bounds, depth, source
custom_searchbounds OR metrodepth, minListings, minEnrichedsource
recent_activitynone (source defaults to agent)source, minEnrichedbounds, 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_SUMMARY states the current coverage on every catalog run so you don't have to track it here. Optionally set metro to 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) or bounds, a lat/lng bounding box for a precise custom area metro doesn't cover: north/south are latitude, east/west are longitude, and north must always be the LARGER (more northerly) value, with south/west the smaller ones. A swapped box (e.g. north given as the smaller number) is rejected immediately with an explanatory error rather than silently returning nothing, and so is an order with neither metro nor bounds set. If you set both, bounds wins. 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; check ORDER_SUMMARY's cities_included on your run to see exactly which ones landed in yours. The only mode where depth: 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) or owner (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, or custom_search with depth: enriched. For custom_search with depth: listings, this is forced to 0 internally 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, minEnriched must 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):

GroupFields
Identity / linkzpid, mls_id, detail_url
Price / statusprice, status_type, status_text
Size / typebedrooms, bathrooms, living_area, home_type, lot_area_value, lot_area_unit
Locationaddress_street, address_city, address_state, address_zipcode, latitude, longitude
Timingyear_built, days_on_zillow
Open househas_open_house, open_house_start, open_house_end
Occupancy/ownership flagsis_non_owner_occupied, is_zillow_owned, country
Listing-type flagsis_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)

EventWhenPrice
actor-startonce per run$0.00001
listing-rowper row at listings depth$0.0007 (~$0.70 per 1000 rows)
enriched-rowper 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:

  1. 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 (

    minListings: 10-50
    ); 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 own waitSecs cap, separate from timeoutSecs), then if the run is still going it returns a status line like "RUNNING for 40s. 2 results so far" plus a nextStep telling you exactly how to poll. Don't treat this as an error; the run is healthy and collecting.

  2. Poll: follow nextStep by calling get-actor-run with the runId and waitSecs: 30 repeatedly until the status reads SUCCEEDED (or TIMED-OUT with partial results). Each poll shows incremental progress (the result count climbs), so you can watch collection advancing.

  3. Fetch: once terminal, call get-dataset-items with the datasetId from 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.