# Zillow Property Scraper: Sale, Rent & Sold Homes (`herus13/zillow-property-scraper`) Actor

Search Zillow listings by city, ZIP or search link and get full property details in the same run: description, photos, price and tax history, agent phone, sale date. Apartments for rent come back one row per unit with leasing phone and office hours. $2.50 per 1,000 homes, details included.

- **URL**: https://apify.com/herus13/zillow-property-scraper.md
- **Developed by:** [herus13](https://apify.com/herus13) (community)
- **Categories:** Real estate
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.75 / 1,000 property 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/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

This Zillow scraper returns homes for sale, sold homes and rentals with full property details in one run: description, photos, price and tax history, and the listing agent's name and phone. There is no second detail run to set up or pay for. $2.50 per 1,000 homes on the Free plan, less on paid plans.

### What you get

**Built for:**

- **Sold-home research**: sale date and county assessed value on sold rows, kept even in states that withhold the sale price.
- **Rental market research**: apartment buildings come back one row per unit, with rent, beds, baths, the leasing office's phone and its office hours.
- **Agent outreach**: listing agent name, phone and brokerage on most for-sale and sold listings.

Search a city or ZIP, paste a Zillow search link, or list property URLs.

**Three listing types, one input.** `for_sale`, `for_rent` and `sold` (sale date and final price where the state discloses it).

**Rentals in apartment buildings come back one row per unit.** Each row has the unit number, rent, beds, baths, sqft, the leasing office's name and phone, its office hours (such as `Mon - Fri: 8:30AM - 5:30PM`) and a `verified_source` flag when Zillow marks the contact as a trusted source. Search a city for rentals; building links are not required.

One dataset row per property, flat enough to open in Excel. Export as JSON, CSV or Excel.

```json
{
  "zpid": "12345678",
  "url": "https://www.zillow.com/homedetails/123-main-st/12345_zpid/",
  "address": "123 Main St, San Francisco, CA 94102",
  "city": "San Francisco",
  "state": "CA",
  "zipcode": "94102",
  "latitude": 37.7749,
  "longitude": -122.4194,
  "price": 1250000,
  "listing_type": "for_sale",
  "status": "for_sale",
  "beds": 3,
  "baths": 2.0,
  "sqft": 1850,
  "year_built": 1965,
  "property_type": "house",
  "days_on_market": 14,
  "price_per_sqft": 676,
  "zestimate": 1300000,
  "rent_zestimate": 5800,
  "agent_name": "Jane Smith",
  "agent_phone": "415-555-0100",
  "brokerage": "Compass",
  "mls_id": "ML81912345",
  "scraped_at": "2026-04-12T10:30:00Z",
  "_meta": {"author": "https://apify.com/herus13", "status": "ok", "message": null}
}
```

`_meta` is run metadata, not scraped data. A target that fails appears as a row with only `_meta` (status `failed`, the reason in `message`); those rows are never charged.

A full row also carries `street`, `lot_size_sqft`, `hoa_fee`, `description`, `features`, `agent_email`, `photos`, `price_history`, `tax_history`, `office_hours`, `verified_source`, `walk_score`, `bike_score`, `transit_score`, `date_sold` and `tax_assessed_value`. `photos` and `features` are lists of strings; `price_history` and `tax_history` arrive as JSON-encoded strings, so a CSV or Excel export opens without a column blowing up. A field Zillow does not publish for a listing is `null`, never guessed:

- `zestimate` and `rent_zestimate` come back from Zillow as `null` for most active for-sale and for-rent listings (Zillow only attaches one to some), and are filled for sold listings.
- `agent_name`, `agent_phone`, `brokerage` and `mls_id` are filled for most for-sale and sold listings; a few sold listings carry no agent phone.
- Rentals in apartment buildings (one row per unit) carry the leasing office as `agent_name` and `brokerage`, its `agent_phone`, `office_hours` (a list such as `"Mon - Fri: 9:00AM - 6:00PM"`, empty when the building lists none) and `verified_source` (`true` when Zillow marks the contact as a trusted source). They have no `description`, `mls_id` or price history.
- `walk_score`, `bike_score` and `transit_score` are filled for for-sale and sold listings and `null` for apartment-building units. A score outside 0-100 is `null`, and `transit_score` is `null` where Zillow has no transit data for the address.
- `date_sold` is the sale date (`YYYY-MM-DD`) of a sold listing, and `null` for every other listing type or when Zillow gives none. It is filled even where `price` is withheld.
- `tax_assessed_value` is the county's assessed value, filled for any listing type where Zillow publishes one and `null` otherwise. It is not a sale price.
- `price` is `null` for sold homes in states that do not disclose sale prices (Texas, Utah, Missouri, Idaho, Wyoming, Montana, New Mexico, North Dakota, Mississippi, Kansas, Alaska and Louisiana were checked; the same applies to any other state that withholds it). The row still carries the address, size, Zestimate, scores and description; some of those rows also lack agent fields.
- `agent_email` is always `null`: Zillow does not publish listing agents' email addresses on any listing type.

#### Feedback

[![Found this useful? Leave a review on Apify - five stars, Herus13](https://api.apify.com/v2/key-value-stores/QTKB7b5ApPQehhhot/records/zillow-review-banner-600.png)](https://apify.com/herus13/zillow-property-scraper/reviews)

Found this useful? [Leave a review on Apify](https://apify.com/herus13/zillow-property-scraper/reviews).

### What it costs

| Event | Free | Bronze | Silver | Gold+ |
|---|---|---|---|---|
| Actor Start (`apify-actor-start`) | $0.00005 | $0.00005 | $0.00005 | $0.00005 |
| property result (`property-result`) | $0.0025 | $0.00225 | $0.002 | $0.00175 |

### Input

| Field | Type | Required | Default | What it does |
|---|---|---|---|---|
| `search_url` | string | no | — | A Zillow search results page to scrape in full, for example `https://www.zillow.com/san-francisco-ca/rentals/`. Build the search on Zillow, copy the URL, paste it here, and every listing it returns becomes a row. The URL carries its own filters, so the filter fields below are not applied to it. Leave empty to search by `location` or to scrape `property_urls` instead. |
| `location` | string | no | — | Where to search, written as city and state or as a ZIP code, for example `San Francisco, CA` or `94102`. Combine it with the listing type and the price, bedroom, bathroom, area and property-type filters below. Leave empty when you are using `search_url` or `property_urls` instead. |
| `listing_type` | string | no | `"for_sale"` | Which listings a `location` search returns: for_sale for homes on the market, for_rent for rentals, sold for recently sold homes with their final price (null where the state does not disclose sale prices, such as Texas). Default for_sale. It has no effect on `search_url` or `property_urls`, which carry their own listing type. |
| `min_price` | integer | no | — | Lowest listing price to keep, in US dollars, 0 or more — for example 500000. Leave empty for no lower bound. Applies to a `location` search only. |
| `max_price` | integer | no | — | Highest listing price to keep, in US dollars, 0 or more — for example 1500000. Leave empty for no upper bound. Applies to a `location` search only. |
| `beds_min` | integer | no | — | Fewest bedrooms a listing may have, 0 or more. Leave empty for no lower bound. Applies to a `location` search only. |
| `beds_max` | integer | no | — | Most bedrooms a listing may have, 0 or more. Leave empty for no upper bound. Applies to a `location` search only. |
| `baths_min` | number | no | — | Fewest bathrooms a listing may have, 0 or more; half baths count as 0.5. Leave empty for no lower bound. Applies to a `location` search only. |
| `baths_max` | number | no | — | Most bathrooms a listing may have, 0 or more; half baths count as 0.5. Leave empty for no upper bound. Applies to a `location` search only. |
| `sqft_min` | integer | no | — | Smallest interior area to keep, in square feet, 0 or more — for example 1000. Leave empty for no lower bound. Applies to a `location` search only. |
| `sqft_max` | integer | no | — | Largest interior area to keep, in square feet, 0 or more — for example 4000. Leave empty for no upper bound. Applies to a `location` search only. |
| `property_type` | array | no | — | Kinds of property to keep, one per entry: `house`, `condo`, `townhouse`, `multi_family`, `land`, `apartment`. Leave empty to keep every kind. Applies to a `location` search only. |
| `days_on_market_max` | integer | no | — | Keep only listings that have been on the market this many days or fewer, at least 1 — 7 gives you the past week, which is what a new-listings alert wants. Leave empty to keep listings of any age. Applies to a `location` search only. |
| `property_urls` | array | no | — | Zillow property pages to scrape exactly, one URL per entry, shaped `https://www.zillow.com/homedetails/123-main-st/12345_zpid/`. Each URL returns one row with the full field set. Leave empty to work from `search_url` or `location` instead. |
| `max_results` | integer | no | `100` | Most properties the whole run may return, from 1 to 10000. Default 100. A Zillow search page holds about 40 listings, and pages are followed until this cap or the end of the results, whichever comes first. It is the ceiling on how much one run collects. |
| `include_photos` | boolean | no | `true` | Add the listing's photo URLs to each row, as `photos`. Default true. Set it to false for a narrower export when you only need the numbers. |
| `include_price_history` | boolean | no | `true` | Add the dates, events and prices behind the current asking price to each row, as `price_history`. Default true. It is what makes a price cut or a relisting visible without scraping the same home twice. Set it to false to leave it out. |
| `proxyUrls` | array | no | — | Leave empty and the run uses the residential proxy this actor ships with, included in the price of the run. To route the run through your own account instead, add one gateway URL per entry, for example http://user:pass@host:port — works with any provider that issues URLs. When set, the run uses these first and switches to the included proxy if they stop working, so a broken proxy never fails the run. |

### How to run it

1. Open the actor on Apify Console and click **Try for free**.
2. Pick one way in: paste a **Search URL**, set a **Location** with filters, or paste **Property URLs**.
3. Choose the listing type and any filters, then Run. Export JSON, CSV or Excel from the dataset.

```json
{
  "location": "San Francisco, CA",
  "listing_type": "for_sale",
  "min_price": 500000,
  "max_price": 1500000,
  "beds_min": 2,
  "property_type": ["house", "condo"],
  "days_on_market_max": 30,
  "max_results": 200
}
```

Minimal input:

```json
{
  "location": "San Francisco, CA"
}
```

Available as the MCP tool `herus13--zillow-property-scraper` on mcp.apify.com and through the Apify API; send the same JSON.

### Why this scraper

- **Three ways in.** A saved Zillow search you already use, a plain location with filters, or exact property URLs — whichever matches how you work.
- **For sale, for rent and sold from one input.** Comparable sales and active inventory land in the same table with the same columns, which is what a comps analysis needs.
- **Price history on the row.** Reductions, relistings and time on market are visible from a single run, instead of having to be reconstructed by scraping the same home week after week.
- **Zestimate next to the asking price.** The estimate and the market's answer sit side by side, which is the pair a pricing model is judged against.
- **Agent and brokerage details included.** Name, phone, brokerage and MLS ID come with most sale and sold listings (no email: Zillow does not publish it), so market activity can be segmented by who is selling it.

### Use cases

- **Real estate databases** — Build and refresh a table of active, rented and sold inventory for a city or ZIP code, with 30 fields per home rather than the handful a listing page shows.
- **Price-change and days-on-market tracking** — Watch reductions and stale inventory per property using `price_history` and `days_on_market`, not a diff of two exports.
- **Valuation modelling** — Train or check a model against `zestimate`, `price`, `price_per_sqft`, `year_built` and `lot_size_sqft` on real inventory.
- **Comparable sales research** — Set the listing type to sold and pull the recent closings in a neighbourhood with their final prices and history.
- **Agent and brokerage lead sourcing** — Find who is active in a market and at what price point, from the agent and brokerage fields on every listing.
- **Rental market analysis** — Compare asking rents against `rent_zestimate` by neighbourhood, bedroom count and square footage.

### FAQ

**Which of the three input modes should I use?** A **Search URL** when you already built the search on Zillow and want exactly what it shows. **Location** plus the filters when you want to describe the search here. **Property URLs** when you have a specific list of homes.

**Can I scrape sold properties?** Yes. Set **Listing Type** to `sold` and you get recently sold homes with their final price (where the state discloses it, see the field notes above) and price history, alongside the same address, size and agent fields as active listings, plus the Zestimate and rent Zestimate that Zillow attaches to sold homes.

**How many results can I get in one run?** Up to 10,000 through **Max Results**. A Zillow search page holds about 40 listings, and pages are followed until the cap or the end of the results.

**Which property types can I filter by?** `house`, `condo`, `townhouse`, `multi_family`, `land` and `apartment`. Pass any combination, or leave the field empty to keep every type.

**Is a proxy included?** A residential proxy is included in the price of the run. To use your own, paste gateway URLs into **Your own proxy URLs**.

**What happens if my input is wrong?** The run stops before it scrapes anything, so no results are charged. Its status and log name each field that is wrong and say what to change — fix the input and run again.

**Is scraping Zillow legal?** This actor collects only publicly available listing data. You are responsible for complying with Zillow's terms and applicable laws. Do not collect personal data without a lawful basis.

### Related actors

Building a wider property or lead pipeline? Pair this actor with:

- [Google Maps Business & Contact Scraper](https://apify.com/herus13/google-maps-scraper) — agents, brokerages and contractors with emails, phone numbers and reviews.
- [LinkedIn Profile Scraper](https://apify.com/herus13/linkedin-profile-scraper) — structured profiles for the agents and brokers you found.
- [Trustpilot Company & Review Scraper](https://apify.com/herus13/trustpilot-company-scraper) — reputation data on brokerages and property-service companies.

# Actor input Schema

## `search_url` (type: `string`):

A Zillow search results page to scrape in full, for example `https://www.zillow.com/san-francisco-ca/rentals/`. Build the search on Zillow, copy the URL, paste it here, and every listing it returns becomes a row. The URL carries its own filters, so the filter fields below are not applied to it. Leave empty to search by `location` or to scrape `property_urls` instead.

## `location` (type: `string`):

Where to search, written as city and state or as a ZIP code, for example `San Francisco, CA` or `94102`. Combine it with the listing type and the price, bedroom, bathroom, area and property-type filters below. Leave empty when you are using `search_url` or `property_urls` instead.

## `listing_type` (type: `string`):

Which listings a `location` search returns: for_sale for homes on the market, for_rent for rentals, sold for recently sold homes with their final price (null where the state does not disclose sale prices, such as Texas). Default for_sale. It has no effect on `search_url` or `property_urls`, which carry their own listing type.

## `min_price` (type: `integer`):

Lowest listing price to keep, in US dollars, 0 or more — for example 500000. Leave empty for no lower bound. Applies to a `location` search only.

## `max_price` (type: `integer`):

Highest listing price to keep, in US dollars, 0 or more — for example 1500000. Leave empty for no upper bound. Applies to a `location` search only.

## `beds_min` (type: `integer`):

Fewest bedrooms a listing may have, 0 or more. Leave empty for no lower bound. Applies to a `location` search only.

## `beds_max` (type: `integer`):

Most bedrooms a listing may have, 0 or more. Leave empty for no upper bound. Applies to a `location` search only.

## `baths_min` (type: `number`):

Fewest bathrooms a listing may have, 0 or more; half baths count as 0.5. Leave empty for no lower bound. Applies to a `location` search only.

## `baths_max` (type: `number`):

Most bathrooms a listing may have, 0 or more; half baths count as 0.5. Leave empty for no upper bound. Applies to a `location` search only.

## `sqft_min` (type: `integer`):

Smallest interior area to keep, in square feet, 0 or more — for example 1000. Leave empty for no lower bound. Applies to a `location` search only.

## `sqft_max` (type: `integer`):

Largest interior area to keep, in square feet, 0 or more — for example 4000. Leave empty for no upper bound. Applies to a `location` search only.

## `property_type` (type: `array`):

Kinds of property to keep, one per entry: `house`, `condo`, `townhouse`, `multi_family`, `land`, `apartment`. Leave empty to keep every kind. Applies to a `location` search only.

## `days_on_market_max` (type: `integer`):

Keep only listings that have been on the market this many days or fewer, at least 1 — 7 gives you the past week, which is what a new-listings alert wants. Leave empty to keep listings of any age. Applies to a `location` search only.

## `property_urls` (type: `array`):

Zillow property pages to scrape exactly, one URL per entry, shaped `https://www.zillow.com/homedetails/123-main-st/12345_zpid/`. Each URL returns one row with the full field set. Leave empty to work from `search_url` or `location` instead.

## `max_results` (type: `integer`):

Most properties the whole run may return, from 1 to 10000. Default 100. A Zillow search page holds about 40 listings, and pages are followed until this cap or the end of the results, whichever comes first. It is the ceiling on how much one run collects.

## `include_photos` (type: `boolean`):

Add the listing's photo URLs to each row, as `photos`. Default true. Set it to false for a narrower export when you only need the numbers.

## `include_price_history` (type: `boolean`):

Add the dates, events and prices behind the current asking price to each row, as `price_history`. Default true. It is what makes a price cut or a relisting visible without scraping the same home twice. Set it to false to leave it out.

## `proxyUrls` (type: `array`):

Leave empty and the run uses the residential proxy this actor ships with, included in the price of the run. To route the run through your own account instead, add one gateway URL per entry, for example http://user:pass@host:port — works with any provider that issues URLs. When set, the run uses these first and switches to the included proxy if they stop working, so a broken proxy never fails the run.

## Actor input object example

```json
{
  "location": "San Francisco, CA",
  "listing_type": "for_sale",
  "property_type": [
    "house",
    "condo"
  ],
  "max_results": 100,
  "include_photos": true,
  "include_price_history": true
}
```

# Actor output Schema

## `results` (type: `string`):

List of property listings with prices, photos, price history, and agent info scraped from Zillow. Each row also carries `_meta` (author, status, message): run metadata, not scraped data.

# 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 = {
    "location": "San Francisco, CA",
    "property_type": [
        "house",
        "condo"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("herus13/zillow-property-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 = {
    "location": "San Francisco, CA",
    "property_type": [
        "house",
        "condo",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("herus13/zillow-property-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 '{
  "location": "San Francisco, CA",
  "property_type": [
    "house",
    "condo"
  ]
}' |
apify call herus13/zillow-property-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,herus13/zillow-property-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/18TsgDA9prOkXREaR/builds/PCh3fOUSsvc06YY1D/openapi.json
