# Zillow Search Scraper — Listings & Agent Contacts (`thenetaji/zillow-search-scraper`) Actor

Every home for sale or recently sold in one US city, state or ZIP code, exported as rows with the price, beds, baths, size, full address and map coordinates. Enrichment adds what the listing card never shows: the listing agent's name, email address and phone number, and the Zestimate.

- **URL**: https://apify.com/thenetaji/zillow-search-scraper.md
- **Developed by:** [The Netaji](https://apify.com/thenetaji) (community)
- **Categories:** Real estate, Lead generation, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Zillow Search Scraper

Every home for sale or recently sold in one US city, state or ZIP code, exported as a spreadsheet of
listings. Each row carries the asking or sale price, the bedroom and bathroom count, the interior
area, the full address, the map coordinates, the listing status and the brokerage behind it.
Enrichment adds the part a Zillow page never shows: the listing agent's name, email address and
telephone number.

A region is paged automatically. How far one region reaches, and what to do when a market runs past
it, is answered under Questions.

### Accepted input

`region` is required and is the region slug from a Zillow URL: the `san-francisco-ca` in
`https://www.zillow.com/san-francisco-ca/`. A bare state such as `ca` and a ZIP code such as `94103`
are regions to Zillow as well, so the field is validated as a slug shape rather than as a
`city-state` pair. A pasted Zillow search URL is accepted in either spelling the site uses and is
reduced to the slug; the `sold` and `2_p` path segments such a URL carries are dropped rather than
folded in, because `status` and `startPage` are separate fields here.

`status` selects `for_sale`, the default, or `sold`. `startPage` sets the page the walk begins from
and defaults to `1`; it is capped at `20`, which is the deepest page Zillow will serve for any
region. `maxItems` bounds the rows saved and defaults to `100`; a value of `0` removes the bound and
lets the walk continue to the last page the region has.

```json
{
  "region": "san-francisco-ca",
  "status": "for_sale",
  "maxItems": 100
}
```

### Response fields

```json
{
  "zpid": "2146999031",
  "url": "https://www.zillow.com/homedetails/2146999031_zpid/",
  "price": 480000,
  "price_display": "$480,000",
  "bedrooms": 3,
  "bathrooms": 2,
  "living_area": 1600,
  "address": "2114 S Trumbull Ave, Chicago, IL 60623",
  "street_address": "2114 S Trumbull Ave",
  "city": "Chicago",
  "state": "IL",
  "zipcode": "60623",
  "latitude": 41.84,
  "longitude": -87.71,
  "status_text": "House for sale",
  "status_type": "FOR_SALE",
  "marketing_status": "For Sale",
  "raw_home_status": "ForSale",
  "broker_name": "RE/MAX Partners",
  "is_featured_listing": false
}
```

Fields absent from a listing are returned as null rather than omitted, so every row has the same
shape.

`zpid` is Zillow's own identifier for the home and is the stable key for a property. It is also the
exact value [Zillow Property Scraper](https://apify.com/thenetaji/zillow-property-scraper) accepts,
which is what makes the two Actors compose: a region is walked here, and each home is then read in
full there without a URL ever being constructed. That matters more on this site than it sounds. The
address half of a Zillow property link is decorative; `/homedetails/2146999031_zpid/` and
`/homedetails/any-slug-at-all/2146999031_zpid/` both resolve to the same home, so a link built from
an address is not validated by anything and can quietly address a different property.

`price` is the number and `price_display` is the string Zillow rendered, including the `/mo` and `+`
decorations it sometimes attaches; `price` is the field to compute with.

`status_text`, `status_type`, `marketing_status` and `raw_home_status` state the same status at
decreasing levels of processing: the rendered label, its machine form, Zillow's simplified marketing
code, and the unmapped value from the feed. `is_featured_listing` reports whether a row is a paid
placement, which is worth reading before result order is treated as a ranking.

`broker_name` is the only attribution a listing row carries. The listing agent's name, email address
and telephone number are on the home's own record, and are covered below.

Open houses arrive as `has_open_house` and `open_house_description`, the window written the way
Zillow writes it, such as `"Sat 1-3pm"`.

### Enriching with property details

`enrichPropertyDetails` reads each listing's own property record and merges it onto the row. It is
off by default, costs one further request per home, and is billed per home enriched, only after the
record has been read.

Two groups of fields are the reason to turn it on, and neither is rendered anywhere on a Zillow page.

The first is who is selling the home: `agent_name`, `agent_email`, `agent_phone`, `broker_phone`,
`mls_id`, `mls_name`, `listing_agreement` and `true_status`. `agent_email` is null more often than
not; a telephone number is stated far more consistently than an email address across the listings I
have measured.

The second is how much attention the listing is getting: `page_view_count`, `favorite_count` and
`tour_view_count`. A save is a logged-in action by somebody who intends to return, so its ratio to
the view count behaves as a conversion rate for the individual home. All three are worth reading
against `days_on_zillow`, because 197 views over 23 days describes a different listing from 197
views in one.

The rest of the record follows as well: `zestimate`, `rent_zestimate`, `last_sold_price`,
`date_sold`, `price_change`, `price_change_date_display`, `tax_annual_amount`, `property_tax_rate`,
`monthly_hoa_fee`, `year_built`, `lot_size`, `lot_area_value`, `lot_area_units`, `home_type`,
`property_type`, `listing_type`, `contingent_listing_type`, `country`, `county`, `neighborhood`,
`community`, `subdivision`, `time_zone`, `description`, `date_posted`, `home_status`, `currency`,
`is_showcase_listing`, `has_virtual_tour`, `photos`, `photo_count` and `reso_facts`.

```json
{
  "region": "https://www.zillow.com/san-francisco-ca/",
  "status": "sold",
  "maxItems": 50,
  "enrichPropertyDetails": true
}
```

Where the record and the listing state the same field, the record wins; it is the home's own
document rather than the card Zillow drew for a results page. A null on the record never overwrites
a value the listing already carried, so a sparse record cannot blank a column partway down a
dataset. The listing-only fields, `address`, `status_text`, `image_url`, the open-house pair and
`is_featured_listing`, are left untouched.

A home whose record cannot be read keeps its listing row unchanged and is not billed for the
enrichment.

### Questions

**How many homes does one region return?**
At most 820, and usually fewer. Zillow serves 41 homes per page and will hand over 20 pages of any
region, so 20 x 41 = 820 is the ceiling, and the last page it serves is normally short. That ceiling
belongs to Zillow rather than to this Actor, and no setting here moves it.

**Zillow says thousands of homes match the region, so why did the run stop at 820?**
Because the two numbers Zillow publishes answer different questions: how many homes match, and how
many pages it is willing to serve. On sold listings they diverge by more than twentyfold. Measured
on San Francisco, `for_sale` reports 773 matching homes across 19 pages, so every one of them is
reachable; `sold` reports 18,802 matching homes across 20 pages, of which 820 are reachable.
Dividing the match count by the page size asks for page 459 of something that stops at 20. The run
log prints both numbers on the first page and says plainly when a region is capped. When it is, the
result is a sample of the matching set rather than an enumeration of it, and rerunning does not
change that.

**How is a market larger than 820 homes covered in full?**
Two routes. A region can be subdivided, because a ZIP code is a region here: `94103` and `94110` are
two separate 20-page allowances rather than one, and a city split into its ZIP codes multiplies the
ceiling by the number of them. Otherwise
[Zillow Sitemap Scraper](https://apify.com/thenetaji/zillow-sitemap-scraper) reads Zillow's own
published index instead of its rendered search pages and has no per-region cap at all; its property
identifiers feed [Zillow Property Scraper](https://apify.com/thenetaji/zillow-property-scraper)
unchanged. That Actor exists because of this ceiling, not as a variation on this one.

**Why did the run return fewer rows than `maxItems`?**
Because the region ran out first. The walk ends when Zillow stops naming a next page rather than
when a page comes back looking thin, which is deliberate: a page served thin partway through a
region is not the end of it.

**Can rentals be collected through `status`?**
No, and that is a measurement rather than a policy. Zillow's rentals listing answers with a
full-size page carrying no property identifiers on it at all; 0 of 3 attempts carried one. Rentals
are keyed by something other than the identifier this Actor composes on, so accepting the value
would return an empty result set for a page that visibly has listings on it, which reads as "there
are no rentals in this city". Rental properties are reachable through
[Zillow Sitemap Scraper](https://apify.com/thenetaji/zillow-sitemap-scraper) with the `for_rent`
family, where they are filed under ordinary property identifiers.

**Why is there an open-house description but no open-house start and end time?**
Zillow ships machine-form start and end values beside the description, and nothing in the capture
pinned down what unit they are in. Unlabelled integers that look like timestamps get read as
timestamps and acted on as though they were, so they are left out and the export carries only
`has_open_house` and `open_house_description`, which state the same window with no guess in them.

**Do pages overlap, and does a walk need de-duplication?**
No. Page 1 and page 2 of a measured region share zero property identifiers, compared as sets rather
than by response size. A repeated identifier is dropped if one ever appears, but that guard has not
fired on any region I have run.

**What happens when a region slug is wrong?**
The run stops with a message naming the shape a region takes, rather than finishing with an empty
dataset. An empty dataset from a mistyped slug is indistinguishable from a genuinely empty region,
which is the worse of the two failures. A region that exists and has no matching homes is the second
case, and it produces a normal run with no rows.

**Why is `broker_name` populated while `agent_name` is empty?**
A listing row carries the brokerage and nothing further about who is selling. The agent's name,
email address and telephone number are on the home's own record, so they require
`enrichPropertyDetails`, or a second pass over the `zpid` column through
[Zillow Property Scraper](https://apify.com/thenetaji/zillow-property-scraper).

**Is a Zillow account, cookie or API key required?**
No. No account, session cookie or key of any kind is supplied to the Actor or needed by it.

### Related Actors

[Zillow Property Scraper](https://apify.com/thenetaji/zillow-property-scraper) reads a home in full
from its identifier, or returns its price history, tax history, schools and comparables. It is the
cheaper route once the identifiers are already in hand, because it spends no requests discovering
them.

[Zillow Sitemap Scraper](https://apify.com/thenetaji/zillow-sitemap-scraper) exports property
identifiers in bulk from Zillow's published index. It is the right choice when the goal is coverage
of a population rather than the current state of one market.

# Actor input Schema

## `region` (type: `string`):

The region slug from a Zillow URL — the `san-francisco-ca` in https://www.zillow.com/san-francisco-ca/. A pasted search URL works too, in either spelling Zillow uses. A bare state (`ca`) and a ZIP code (`94103`) are regions here as well.

## `status` (type: `string`):

Which listings to read: homes on the market, or homes that have sold.

## `startPage` (type: `integer`):

Which page of the region the walk starts from, at 41 homes per page. It resumes an interrupted walk, and it splits one region across several runs. Zillow serves at most 20 pages for any region.

## `maxItems` (type: `integer`):

Maximum number of rows to save. Set 0 for no limit.

## `enrichPropertyDetails` (type: `boolean`):

Reads each listing's own property record and merges it onto the row: the listing agent's name, email and phone number, the MLS name and listing identifier, the Zestimate and rent Zestimate, the tax bill and rate, the HOA fee, the year built, the lot size, the county, the listing description, the page view, favourite and tour counts, every photograph, and the complete RESO fact set. It costs one further request per home, so a run is slower with it on, and it is billed per home enriched.

## Actor input object example

```json
{
  "region": "san-francisco-ca",
  "status": "for_sale",
  "startPage": 1,
  "maxItems": 20,
  "enrichPropertyDetails": false
}
```

# Actor output Schema

## `dataset` (type: `string`):

All records scraped by this run

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "region": "san-francisco-ca",
    "status": "for_sale",
    "startPage": 1,
    "maxItems": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("thenetaji/zillow-search-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "region": "san-francisco-ca",
    "status": "for_sale",
    "startPage": 1,
    "maxItems": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("thenetaji/zillow-search-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "region": "san-francisco-ca",
  "status": "for_sale",
  "startPage": 1,
  "maxItems": 20
}' |
apify call thenetaji/zillow-search-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,thenetaji/zillow-search-scraper"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/9QMRWfn6dLAavdusQ/builds/BeDlb5LVrUS0Tg6WS/openapi.json
