# Zillow Scraper — US Homes for Sale, Rent & Sold by ZIP or City (`diopside/zillow-us`) Actor

Drop-in compatible with maxcopell/zillow-zip-search and maxcopell/zillow-scraper — cheaper, and ZIP, city and URL search in one actor. Zillow listings as JSON: price, beds/baths, area, lot, status, days on Zillow, coordinates, photos, broker. Optional detail pass adds price history and MLS id.

- **URL**: https://apify.com/diopside/zillow-us.md
- **Developed by:** [DIOPSIDE AI](https://apify.com/diopside) (community)
- **Categories:** Real estate
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 property listings

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Zillow Scraper — US homes for sale, rent & sold

**Drop-in compatible with `maxcopell/zillow-zip-search` and `maxcopell/zillow-scraper` — cheaper,
and ZIP, city and URL search live in one actor instead of three.** Same input field names, same
output record shape. Switching is a one-line change of the actor id.

Give it ZIP codes, plain-text places, or Zillow search URLs. Get back every listing as JSON:
price, beds, baths, living area, lot size, home type, status, days on Zillow, coordinates, photos
and the listing brokerage. Turn on **Scrape property details** and each record also carries the
property's price history, the listing description, year built, price per square foot, MLS id and
brokerage — data that today needs a separate actor at $0.0036/result.

### Why this one

| | This actor | `maxcopell/zillow-zip-search` | `maxcopell/zillow-scraper` | `maxcopell/zillow-detail-scraper` |
|---|---|---|---|---|
| Search by ZIP | yes | yes | no | no |
| Search by city / neighborhood name | yes | no | no | no |
| Search by pasted Zillow URL | yes | no | yes | no |
| Beds / baths / square-footage filters | yes | no | no | no |
| Price history, description, MLS id | yes (optional) | no | no | yes |
| Price per result (free tier) | **$0.001**, $0.002 with details | $0.0024 | $0.0023 | $0.0036 |
| Proxy required | no | — | — | — |

Three more things it does differently:

- **It refuses to hand you the wrong rows.** Zillow answers `200 OK` for filter URLs it then
  ignores — ask for `/austin-tx-78704/3-_beds/` and you get the *unfiltered* 354 results with an
  empty filter state. Before pushing a single record from a status-scoped search, this actor
  checks Zillow's own echo of the filter it applied, and fails the target loudly if it does not
  match.
- **A run that produced nothing is a failed run**, and a partial run says in its status message
  exactly which searches came up empty and why.
- **It resolves your location against Zillow**, not against a guess. `78704` is
  `austin-tx-78704`, `Brooklyn, NY` is `brooklyn-new-york-ny`, `Wicker Park, Chicago, IL` is a
  neighborhood region — no rule generates all three, so the actor reads the canonical slug back
  off Zillow's own pagination link and records it in `searchRegion`.

### Switching from maxcopell/zillow-zip-search or maxcopell/zillow-scraper

Change the actor id. Nothing else. `zipCodes`, `searchUrls`, `priceMin`, `priceMax`,
`daysOnZillow`, `forSaleByAgent`, `forSaleByOwner`, `forRent`, `sold` and `resultsLimit` all mean
what they mean there, and every field in the output record is present with the same name, nesting
and type. New fields (`searchQuery`, `searchStatus`, `searchRegion`, `isBuilding`, `buildingName`,
`units`, `hasDetails`, `propertyDetails`) are added alongside; nothing of theirs is renamed.

One difference worth knowing: a `?searchQueryState=` URL is reduced to the place you typed into
Zillow, because `zillow.com/robots.txt` disallows that route (see *Which pages this reads*). The
map rectangle in such a URL is not reproduced — search by ZIP, city or neighborhood instead.

### Input

| Field | Type | Default | What it does |
|---|---|---|---|
| `zipCodes` | string\[] | — | US ZIP codes to search. |
| `searchTerms` | string\[] | — | Cities, neighborhoods or counties in plain text: `Austin, TX`. |
| `searchUrls` | request list | — | Zillow search pages to walk. |
| `forSaleByAgent` | boolean | `true` | Include agent-listed homes for sale. |
| `forSaleByOwner` | boolean | `false` | Include FSBO homes. |
| `forRent` | boolean | `false` | Include rentals (a separate Zillow search per location). |
| `sold` | boolean | `false` | Include recently sold homes (a separate search per location). |
| `priceMin` / `priceMax` | integer | — | Price bounds, USD; monthly rent for rentals. |
| `bedsMin` / `bedsMax` | integer | — | Bedroom bounds. |
| `bathsMin` | integer | — | Minimum bathrooms. |
| `sqftMin` / `sqftMax` | integer | — | Living-area bounds, square feet. |
| `homeTypes` | string\[] | all | `SINGLE_FAMILY`, `CONDO`, `TOWNHOUSE`, `MULTI_FAMILY`, `APARTMENT`, `MANUFACTURED`, `LOT`. |
| `daysOnZillow` | select | any | `1`, `7`, `14`, `30`, `90`, `6m`, `12m`, `24m`, `36m`. |
| `scrapePropertyDetails` | boolean | `false` | Open each property page for price history and full facts. |
| `resultsLimit` | integer | 20 | Maximum listings **per location**, shared evenly across the statuses you ticked. |
| `maxItems` | integer | — | Hard cap for the whole run. |
| `proxyConfiguration` | proxy | off | Optional. See *Proxy*. |
| `residentialFallback` | boolean | `true` | Escalate to RESIDENTIAL/US once if a tier is blocked. |
| `maxConcurrency` / `maxRequestsPerMinute` / `maxRequestRetries` | integer | 3 / 30 / 3 | Politeness and resilience. |

Ticking both `forSaleByAgent` and `forSaleByOwner` costs one Zillow search, not two — its default
for-sale page already holds both. `forRent` and `sold` are separate searches per location.

### Output

```json
{
  "zpid": "58312593",
  "listingPrice": { "amount": 625000, "currency": "USD", "formatted": "$625,000" },
  "listingAddress": {
    "street": "3618 S 2nd St", "unit": null, "city": "Austin", "state": "TX",
    "zipCode": "78704", "county": null, "country": "USA", "neighborhood": null,
    "subdivision": null, "communityName": null, "timeZone": null,
    "full": "3618 S 2nd St, Austin, TX 78704"
  },
  "coordinates": { "latitude": 30.230879, "longitude": -97.77021 },
  "listingStatus": "forSale",
  "propertyUrl": "https://www.zillow.com/homedetails/3618-S-2nd-St-Austin-TX-78704/58312593_zpid/",
  "homeType": "SINGLE_FAMILY",
  "listingType": { "isFSBA": true, "isFSBO": false, "isComingSoon": false, "isPending": null,
                   "isOpenHouse": false, "isNewHome": false, "isForAuction": false,
                   "isForeclosure": false, "isBankOwned": false, "contingencyType": null },
  "bedrooms": 3, "bathrooms": 3, "livingArea": 1454, "livingAreaUnit": "sqft",
  "lotArea": 6695.172, "lotAreaUnit": "sqft",
  "daysOnZillow": 6, "taxAssessedValue": 619676,
  "mainImage": "https://photos.zillowstatic.com/fp/afe48247edbd24c47179c3328401db51-p_e.jpg",
  "listingPhotos": [{ "url": "https://photos.zillowstatic.com/fp/afe482...-p_e.jpg", "caption": null }],
  "photoCount": 31,
  "broker": { "name": "LPT Realty LLC - Ascend Group", "phoneNumber": null,
              "buyerBrokerName": null, "buyerAgentName": null },
  "sourceSearchUrl": "https://www.zillow.com/78704/",
  "scrapedAt": "2026-09-21T08:14:25Z",
  "isValid": true,

  "searchQuery": "78704",
  "searchStatus": "forSale",
  "searchRegion": { "regionId": 92617, "regionType": 7, "name": "78704",
                    "displayName": "Austin TX 78704" },
  "hasDetails": true,
  "propertyDetails": {
    "description": "The classic two-story Austin home establishes an inviting presence…",
    "yearBuilt": 2001, "lotSize": 6695, "pricePerSquareFoot": 430,
    "priceHistory": [{ "date": "2026-09-14", "price": 625000, "event": "Listed for sale",
                       "pricePerSquareFoot": 430, "source": "Agent Provided" }],
    "mlsId": "9067839", "mlsName": "Unlock MLS",
    "brokerageName": "LPT Realty LLC", "photoCount": 31
  }
}
```

An apartment-community card has no single price; it comes back with `isBuilding: true`, a
`buildingName`, `availableUnitCount` and a `units` array of floorplans, with `listingPrice.amount`
set to the lowest rent in the building.

### What it does not collect

- **No personal data.** Homeowner names, phone numbers and email addresses are not collected.
  `broker.name` is a business name, and `propertyDetails.agentName` / `agentPhoneNumber` come
  from the MLS attribution line Zillow prints on the public page — where Zillow withholds them,
  they are `null` and stay `null`.
- **No tax history.** Zillow loads the year-by-year tax table from a GraphQL route its robots.txt
  disallows. The county's assessed value is in `taxAssessedValue`, which the search page carries.
- **No sold prices in non-disclosure states.** In Texas and a dozen other states Zillow does not
  publish what a home sold for, so `listingSoldPrice` on those records is `null` — that is
  Zillow's data, not a gap in the scraper.
- **Price history is not on every home.** Zillow serves it for roughly a third of listings;
  `propertyDetails.priceHistory` is an empty list for the rest.

### Which pages this reads

`zillow.com/robots.txt` disallows `/api/`, `/graphql/`, `/async-create-search-page-state`, any URL
carrying `?searchQueryState=`, and the `/homes/` tree. This actor stays off all of them. It reads
two sanctioned page types:

- the region search page, `zillow.com/austin-tx/` and `zillow.com/austin-tx/2_p/`, with the
  `/fsbo/`, `/rentals/`, `/sold/` and `/houses/` variants;
- the property page, `zillow.com/homedetails/…/<zpid>_zpid/`, when details are on.

Both are server-rendered and carry the same JSON the site's own front end uses, so nothing is lost
by staying on them. The consequence you can see is that price, bedroom, bathroom, size and
days-on-market filters are applied by the actor after fetching rather than by Zillow — on the
region route Zillow ignores those URL segments. Narrow searches therefore cost the same number of
requests as wide ones, but **you are only charged for records that pass your filters**.

### Sizes, limits and cost

- 41 listings per request; Zillow stops paginating a single search after **20 pages (~820
  results)**. For a large metro, search by ZIP rather than by city to get past that ceiling.
- A search page is ~650 KB, a property page ~700 KB. A details-off run of 820 listings is 20
  requests and about 13 MB.
- Pay-per-event: **$0.001 per listing**, **$0.002 per listing with details**, plus $0.00005 per
  run start. 1,000 listings cost $1.00 — against $2.40 on `zillow-zip-search`.

### Proxy

**Not required.** Zillow's bot protection reads the TLS fingerprint before it reads the IP, and
this actor presents a real browser fingerprint over plain HTTP — no browser is launched, which
also means no Chromium memory bill. Turn a proxy on for large runs: a single exit IP asked for
hundreds of pages in a row will eventually be rate-limited, and the actor will then rotate
sessions rather than making progress. `residentialFallback` moves the run to RESIDENTIAL/US once
if the configured tier is blocked outright.

### Use cases

- Track price cuts and days-on-market across a farm area, daily, by ZIP.
- Build a comparables set: search `sold`, keep `listingSoldPrice`, `livingArea` and
  `propertyDetails.pricePerSquareFoot`.
- Feed a rental pricing model from `forRent` searches, including whole apartment communities and
  their floorplan rents.
- Monitor new inventory with `daysOnZillow: "1"` on a schedule.
- Enrich an MLS pipeline: match on `propertyDetails.mlsId`, add Zestimate and price history.

# Actor input Schema

## `zipCodes` (type: `array`):

US ZIP codes to search. Same field name as maxcopell/zillow-zip-search, so an existing input works unchanged.

## `searchTerms` (type: `array`):

Cities, neighborhoods or counties in plain text — 'Austin, TX', 'Beverly Hills CA', 'Wicker Park, Chicago, IL'. Zillow resolves each to its own canonical region at run time.

## `searchUrls` (type: `array`):

Zillow search pages to walk. Same field name as maxcopell/zillow-scraper. Region URLs (zillow.com/austin-tx/) are followed as given; ?searchQueryState= URLs are reduced to the place you typed, because robots.txt disallows that route.

## `forSaleByAgent` (type: `boolean`):

Include homes listed for sale by a real estate agent.

## `forSaleByOwner` (type: `boolean`):

Include homes listed for sale by their owner (FSBO).

## `forRent` (type: `boolean`):

Include rental listings. Runs as a separate Zillow search per location.

## `sold` (type: `boolean`):

Include recently sold homes. Runs as a separate Zillow search per location.

## `priceMin` (type: `integer`):

Lowest price to keep (monthly rent for rentals). Applied to Zillow's own price field.

## `priceMax` (type: `integer`):

Highest price to keep (monthly rent for rentals).

## `bedsMin` (type: `integer`):

Fewest bedrooms to keep. The ZIP incumbent has no bedroom filter at all.

## `bedsMax` (type: `integer`):

Most bedrooms to keep.

## `bathsMin` (type: `integer`):

Fewest bathrooms to keep.

## `sqftMin` (type: `integer`):

Smallest living area to keep, in square feet.

## `sqftMax` (type: `integer`):

Largest living area to keep, in square feet.

## `homeTypes` (type: `array`):

Keep only these Zillow home types. Leave empty for all.

## `daysOnZillow` (type: `string`):

Keep only listings newer than this (for sold homes, sold more recently than this).

## `scrapePropertyDetails` (type: `boolean`):

Open each property's own page and add price history, the listing description, year built, lot size, price per square foot, MLS id and brokerage. One extra request per listing, and these records are charged as 'listing with details'.

## `resultsLimit` (type: `integer`):

Maximum listings per location. Same meaning as the incumbent's field: with resultsLimit=10 and 5 ZIP codes you get up to 50 records.

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

Hard cap on records for the entire run, across every location. Leave empty for no cap.

## `proxyConfiguration` (type: `object`):

Zillow's bot protection reads the TLS fingerprint before the IP, so this actor works with the proxy off. Turn a proxy on for large runs: one exit IP asked for hundreds of pages will eventually be rate-limited.

## `residentialFallback` (type: `boolean`):

If every retry on the configured proxy tier is blocked, move the rest of the run to RESIDENTIAL/US once. Residential bandwidth is billed by Apify, so a healthy run never touches it.

## `maxConcurrency` (type: `integer`):

Parallel requests to Zillow.

## `maxRequestsPerMinute` (type: `integer`):

Request rate ceiling. Zillow rate-limits a single exit IP, so raising this without a proxy invites 403s.

## `maxRequestRetries` (type: `integer`):

Attempts per page before the target is reported as failed. Each retry rotates to a new session.

## Actor input object example

```json
{
  "zipCodes": [
    "78704",
    "90210"
  ],
  "searchTerms": [
    "Austin, TX"
  ],
  "searchUrls": [
    {
      "url": "https://www.zillow.com/austin-tx-78704/"
    }
  ],
  "forSaleByAgent": true,
  "forSaleByOwner": false,
  "forRent": false,
  "sold": false,
  "priceMin": 200000,
  "priceMax": 800000,
  "bedsMin": 3,
  "daysOnZillow": "",
  "scrapePropertyDetails": false,
  "resultsLimit": 10,
  "proxyConfiguration": {
    "useApifyProxy": false
  },
  "residentialFallback": true,
  "maxConcurrency": 3,
  "maxRequestsPerMinute": 30,
  "maxRequestRetries": 3
}
```

# Actor output Schema

## `listings` (type: `string`):

All listing records. Append ?format=csv for CSV.

## `datasetUrl` (type: `string`):

The default dataset.

# 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 = {
    "zipCodes": [
        "78704"
    ],
    "resultsLimit": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("diopside/zillow-us").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 = {
    "zipCodes": ["78704"],
    "resultsLimit": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("diopside/zillow-us").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 '{
  "zipCodes": [
    "78704"
  ],
  "resultsLimit": 20
}' |
apify call diopside/zillow-us --silent --output-dataset

```

## MCP server setup

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

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/JZzGkYHJrZKsxB07z/builds/IEifs2etvnbeKPygO/openapi.json
