# Zillow Scraper (`mlg14/zillow-scraper`) Actor

Extract public Zillow property details from listing URLs, addresses, and city search pages. Export prices, status, location, home facts, photos, listing attribution, and available history.

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

## Pricing

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

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

Scrape Zillow property details from public listing URLs, street addresses, or city search pages. Export Zillow data to CSV, JSON, or Excel for pricing research, listing monitoring, and property comparisons. This actor is a practical Zillow API alternative for public listing information: a direct detail page can provide listing facts and attribution, while a search page can deliver a larger set of concise property records.

Each output row represents one property. The `detailAvailable` field tells you whether the actor reached its detail page. This distinction matters: property pages can show description, listing attribution, parking, and more photos, while search cards supply price, address, status, home type, beds, baths, area, and other summary facts. If a detail page is blocked, the actor retains the search card record instead of silently dropping the property.

### What data can you extract from Zillow?

The dataset uses stable top level fields. Values depend on the property type, listing status, and what the public page exposes. An empty value does not imply the property lacks that characteristic. It means the relevant public page did not supply the field at collection time.

| Field | Description | Example |
| --- | --- | --- |
| `zpid` | Zillow property identifier. | 11026031 |
| `detailAvailable` | True when the property detail page was read; false when only search card data was available.. | true |
| `propertyUrl` | Canonical property detail URL. | https://www.zillow.com/homedetails/17-Zelma-Dr-Greenville-SC-29617/... |
| `inputUrl` | URL requested for this property. | https://www.zillow.com/homedetails/17-Zelma-Dr-Greenville-SC-29617/... |
| `sourceSearchUrl` | Search page that yielded the property, when applicable. | null |
| `listingStatus` | Current property status as reported on the page. | FOR_SALE |
| `homeType` | Property type code. | SINGLE_FAMILY |
| `listingType` | Human readable listing category. | For Sale by Agent |
| `isShowcaseListing` | Whether the property uses a showcase listing. | false |
| `isForSaleByOwner` | Whether the property is listed by its owner. | false |
| `price` | Displayed property price in the listed currency. | 487500 |
| `currency` | ISO currency code. | USD |
| `priceChange` | Most recent absolute price change. | -7500 |
| `priceChangedAt` | Date of the most recent price change. | 2026-09-21 |
| `lastSoldPrice` | Price of the last sale when available. | 415000 |
| `zestimate` | Estimated property value when shown. | 478900 |
| `rentZestimate` | Estimated monthly rent when shown. | 2755 |
| `bedrooms` | Bedroom count. | 4 |
| `bathrooms` | Bathroom count. | 3 |
| `bathroomsFull` | Full bathroom count. | 3 |
| `bathroomsHalf` | Half bathroom count. | 0 |
| `livingArea` | Interior area in the displayed unit. | 2582 |
| `livingAreaUnit` | Interior area unit, usually square feet. | null |
| `yearBuilt` | Year of original construction. | null |
| `lotAreaValue` | Numeric lot area. | 0.5 |
| `lotAreaUnit` | Lot area unit. | Acres |
| `lotSizeSqft` | Lot size in square feet when provided. | 21780 |
| `pricePerSquareFoot` | Price divided by interior square feet when supplied. | 189 |
| `streetAddress` | Street address. | 17 Zelma Dr |
| `city` | City. | Greenville |
| `state` | State abbreviation. | SC |
| `zipCode` | Postal ZIP code. | 29617 |
| `country` | Country. | USA |
| `fullAddress` | Combined public property address. | 17 Zelma Dr, Greenville, SC 29617 |
| `latitude` | Latitude of the property. | 34.91575 |
| `longitude` | Longitude of the property. | -82.44426 |
| `neighborhood` | Neighborhood or parent region. | 29617 |
| `timeZone` | Local time zone. | America/New_York |
| `description` | Public listing description. | Public listing text |
| `datePosted` | Date the listing was posted. | 2026-08-09 |
| `dateSold` | Date of the last sale. | 2023-07-10 |
| `daysOnZillow` | Days the listing has been displayed. | 48 |
| `pageViewCount` | Reported page views. | 984 |
| `favoriteCount` | Reported saves or favorites. | 125 |
| `monthlyHoaFee` | Monthly homeowners association fee. | null |
| `taxAssessedValue` | Assessed property tax value. | null |
| `propertyTaxRate` | Property tax rate when shown. | 0.51 |
| `parkingSpaces` | Total parking capacity. | 0 |
| `hasGarage` | Whether a garage is present. | false |
| `parkingFeatures` | Parking features listed in public facts. | \["Driveway", "Parking Pad"] |
| `agentName` | Public listing agent name. | Available when published |
| `agentPhoneNumber` | Public listing agent business phone. | Available when published |
| `agentEmail` | Public listing agent business email when shown. | Available when published |
| `brokerName` | Listing brokerage name. | Available when published |
| `mlsId` | Listing identifier at the listing source. | 1600300 |
| `mlsName` | Name of the listing source. | Available when published |
| `photoCount` | Reported number of photos. | 47 |
| `mainImageUrl` | Main listing photo URL. | Available when published |
| `photoUrls` | Public photo URLs when requested. | \["https://photos.zillowstatic.com/..."] |
| `virtualTourUrl` | Public virtual tour URL. | Available when published |
| `priceHistory` | Price history events exposed in the page payload. | \[] |
| `scrapedAt` | UTC timestamp when the record was collected. | UTC timestamp |

The actor preserves numeric values as numbers rather than adding currency signs or units to them. `price` and `currency` can be used together for calculations. `livingArea`, `lotAreaValue`, and `lotAreaUnit` should be read together because acreage and square footage are different units. The `priceHistory` array is present only when exposed in the embedded detail data; an empty array does not mean there has never been a price change.

### How to scrape Zillow

1. Add one or more property detail URLs, a city search URL, or full street addresses. A typical property URL contains `/homedetails/` and ends with a numeric `_zpid` identifier. A city search URL such as `https://www.zillow.com/greenville-sc/` yields many candidate properties.
2. Set `maxItems` to cap dataset records. For a city search, choose `maxSearchPages` and the desired `propertyStatus`. Start with one page and a modest item limit before expanding a collection.
3. Leave `enrichSearchResults` enabled if you want the actor to open each property discovered on a search page. Turn it off if search card facts are sufficient or if the detail pages are heavily blocked. Direct property URLs are always opened as detail pages.
4. Run the actor and open the dataset. Export the table as JSON, CSV, or Excel. Check `detailAvailable` before relying on description, agent, parking, or history fields.

The actor removes duplicate property identifiers within a run. If a direct URL and a search page point to the same property, only one row is saved. For a direct URL, the record contains `inputUrl` equal to the requested URL and `sourceSearchUrl` is empty. For properties discovered through a city page, `sourceSearchUrl` records that page.

### Input

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `startUrls` | URL list | Empty | Public Zillow property detail or city search URLs. |
| `addresses` | String list | Empty | Full street addresses to resolve through public search results. |
| `propertyStatus` | Select | `ANY` | Filter property cards expanded from a search page: any, for sale, for rent, or recently sold. |
| `maxSearchPages` | Integer | `1` | Maximum search result pages followed, up to 20. |
| `maxItems` | Integer | `100` | Maximum property rows saved. Set zero to remove the item cap. |
| `enrichSearchResults` | Boolean | `true` | Open property detail pages found in search results. |
| `includePhotos` | Boolean | `true` | Include available photo URLs in `photoUrls`. |
| `proxyConfiguration` | Object | Enabled | Connection settings for remote requests. |

A single direct property URL is the best input when you need all publicly exposed details for one listing. A city search URL is better when you need a table of comparable properties. Addresses are resolved by the site's public search path, which may redirect to a matching property. If an address is ambiguous, use a direct property URL to prevent a different match.

Example input for 30 for-sale records from a city search page:

```json
{
  "startUrls": [
    { "url": "https://www.zillow.com/greenville-sc/" }
  ],
  "propertyStatus": "FOR_SALE",
  "maxSearchPages": 1,
  "maxItems": 30,
  "enrichSearchResults": false,
  "includePhotos": true
}
```

For detailed records, change `enrichSearchResults` to `true` or provide the detail URLs themselves. A search run can contain a mix of detailed and summary records because some detail pages may be blocked while the search page remains available. The field `detailAvailable` makes this visible per row.

### Output example

The following selected fields came from a real detail record in the 30 item verification run on September 27, 2026. The listing description and photo list are shortened here for readability. The dataset includes every field listed above, including empty values.

```json
{
  "zpid": 11026031,
  "detailAvailable": true,
  "propertyUrl": "https://www.zillow.com/homedetails/17-Zelma-Dr-Greenville-SC-29617/11026031_zpid/",
  "listingStatus": "FOR_SALE",
  "homeType": "SINGLE_FAMILY",
  "listingType": "For Sale by Agent",
  "price": 487500,
  "currency": "USD",
  "priceChange": -7500,
  "priceChangedAt": "2026-09-21",
  "lastSoldPrice": 415000,
  "zestimate": 478900,
  "rentZestimate": 2755,
  "bedrooms": 4,
  "bathrooms": 3,
  "livingArea": 2582,
  "lotAreaValue": 0.5,
  "lotAreaUnit": "Acres",
  "fullAddress": "17 Zelma Dr, Greenville, SC 29617",
  "latitude": 34.91575,
  "longitude": -82.44426,
  "datePosted": "2026-08-09",
  "dateSold": "2023-07-10",
  "daysOnZillow": 48,
  "photoCount": 47,
  "description": "Location doesn't get better than this!..."
}
```

That property had a detailed page available. A record from the same city search can show `detailAvailable: false` while still carrying `zpid`, status, price, address, home type, bedrooms, bathrooms, area, and estimates from the search card. Treat the two kinds of records differently when building a report that requires complete listing descriptions or attribution.

### Use cases

- **Comparable listing research:** collect active listings in a city, then group homes by location, property type, interior area, beds, and price. Use `pricePerSquareFoot` where the detail page supplies it, or calculate it from numeric price and area.
- **Price movement monitoring:** schedule repeated runs for a stable set of detail URLs and compare `price`, `priceChange`, `priceChangedAt`, and `listingStatus` across dated datasets. A single run is a snapshot, not a historical archive.
- **Property intake:** enter addresses or property URLs from a research list and standardize the output into fields suitable for a spreadsheet. Keep `zpid` as the matching key when refreshing records.
- **Rental and sale comparisons:** use `rentZestimate` and `zestimate` where the site publishes them, alongside listed price and property facts. Estimates are site-provided values, not independent valuations.
- **Listing quality review:** identify records with missing photos, description, construction year, or public attribution. Filter on `detailAvailable` before treating missing detail fields as a listing quality problem.
- **Geographic review:** plot latitude and longitude for properties where the public listing discloses coordinates. Check `fullAddress` before sharing a location externally.

### How much does it cost to scrape Zillow?

The configured dataset event price is **$1 per 1,000 delivered property records**, or $0.001 per saved row. The count refers to output records, not to every page request. A 30 row run has an event charge of about **$0.03**; 1,000 rows cost **$1.00**; and 5,000 rows cost **$5.00** at that rate. Your account's live pricing page is the authority for any separate platform or proxy charges.

A search page can contain dozens of property cards, so `enrichSearchResults: false` can produce a larger dataset with fewer requests. When enrichment is on, each property detail request adds processing time. It does not add an extra result charge for the same property: a property identifier is written once. Residential proxy escalation can also make a blocked detail run more expensive to operate. Start with a smaller `maxItems` value to measure the actual run time and availability for your target area.

### Tips for best results

Use precise property URLs when you need detail fields. The city search path is useful for discovery, but the public card has less information than a property detail page. A full street address with city, state, and ZIP is more reliable than a partial street name; the public resolver can redirect an ambiguous query to another property.

For a large city, set a small `maxItems` and `maxSearchPages: 1` first. Search pages may expose a limited number of results and a finite number of anonymous pages. Increase `maxSearchPages` only if the initial output shows that your page and status combination yields useful results. The actor follows the search page's next link rather than inventing page numbers.

Check `detailAvailable` before deciding which fields to require. If it is false, the row came from embedded search card data. Price, status, address, home type, bed and bath count, interior area, coordinates, and estimates can still be present. Description, full photo sets, parking details, and source attribution usually require a detail page. For strict detail-only pipelines, keep only rows where `detailAvailable` is true and rerun blocked URLs separately later.

Use the date and unit fields carefully. `datePosted`, `dateSold`, and `priceChangedAt` are dates from the public page and can be empty. `scrapedAt` is the collection timestamp. `lotAreaValue` may be in acres while `livingArea` is in square feet. Do not add those measurements together or compare them without converting units.

### Limits

The site may return a block page or a rate limit response, especially when many detail pages are opened in one run. The actor starts with a lower cost connection route, rotates the connection after repeated blocks, and may use a residential route when necessary. Search cards are saved as fallback when their detail pages cannot be read. Direct detail URLs have no search card fallback unless the same property also appears in a search input.

The search expander currently follows public city search pages and emits property detail records identified by a `_zpid` URL. Some rental search pages mostly return apartment or building pages with a different data structure; those entries are skipped. Community and building unit extraction are outside this version's verified scope. A result page can also return fewer rows than requested because of search page limits, duplicate property identifiers, unsupported result types, or blocked pages.

Not every listing publishes every field. A sold or off-market property may not have a current listing price, current agent, or current description. Some property layouts expose price history while others do not. A value may also change between runs as the listing changes. Use `zpid`, `scrapedAt`, and `detailAvailable` to compare snapshots responsibly.

### Automated workflows

A scheduled workflow can collect a small city sample each morning and compare the new dataset to the previous day's dataset by `zpid`. For example: “Collect the first 30 for-sale properties in Greenville and report newly listed properties and price changes.” The comparison step should use the saved datasets because this actor returns the current page snapshot.

A second workflow can enrich a curated list of property URLs: “Open these 20 property detail links and return prices, bedroom counts, interior area, photos, and publicly displayed listing attribution.” Set `maxItems` to match the list length, then flag any row missing from the dataset for retry. Do not assume a blocked direct URL produced a summary record unless a search page also supplied it.

### FAQ

#### Do I need a login?

No. The actor uses public pages. It does not read private account pages or require account credentials for the target site. Public availability still varies with status, location, and traffic controls.

#### Why is `detailAvailable` false?

The row was built from a public search card. This occurs when `enrichSearchResults` is disabled or when the detail page could not be read after connection retries. Core card fields remain useful, but detail-only fields may be empty.

#### Can I collect all results from a large city?

The actor follows up to 20 search result pages per run when they are available. The site can cap anonymous search results, and a city may contain more listings than those pages expose. Narrow inputs by city, status, or other public search pages if you need broader coverage.

#### Does an empty `priceHistory` mean no price changes?

No. It means the embedded page payload did not expose a history array for that record. The current `priceChange` and `priceChangedAt` fields may still be populated separately.

#### Can I export or schedule the data?

Yes. The dataset can be downloaded in JSON, CSV, and Excel formats and accessed through its dataset endpoint. Schedule repeated runs to maintain time snapshots. Compare them by `zpid` rather than by address text, which can be reformatted.

#### Is this data safe to reuse?

Only public page data is collected. Follow the site's terms and applicable privacy laws, and avoid using property or professional contact details for unwanted outreach. Decide the permitted use of the exported data for your project and location.

#### What if an address resolves to the wrong home?

Use the direct property URL. Address lookup relies on the site's public search redirect and may choose another match when the address is incomplete or ambiguous. Check `propertyUrl`, `zpid`, and `fullAddress` after a lookup.

### Integrations

The dataset endpoint can feed reporting jobs, spreadsheets, scheduled comparisons, and webhook based workflows. A common pattern is to run the actor on a schedule, export the latest dataset, match rows by `zpid`, and write only changed prices or statuses to the destination. Keep the `detailAvailable` flag in downstream tables so analysts can distinguish a compact search record from a fuller property record.

### Support

Open an issue on the Issues tab; we reply within 24h and add fields on request. Include a public property URL, the run date, and the field you expected. This makes it possible to distinguish a missing public field from a parser change or a temporary block.

# Actor input Schema

## `startUrls` (type: `array`):

Public Zillow property detail URLs or city search pages. Search pages expand into property detail records.

## `addresses` (type: `array`):

Street addresses to resolve through public Zillow results. Use full street, city, state, and ZIP for best matches.

## `propertyStatus` (type: `string`):

Limit listings expanded from search pages by status. Direct property URLs always return their actual status.

## `maxSearchPages` (type: `integer`):

Maximum city search result pages to follow. Zillow may cap anonymous search results.

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

Stop after this many property records. Zero removes the item cap.

## `includePhotos` (type: `boolean`):

Include all photo URLs exposed in the property page data.

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

Use Apify Proxy. Datacenter is tried first and residential is used after repeated blocks.

## `enrichSearchResults` (type: `boolean`):

Open each property found on a search page for additional fields. Blocked detail pages fall back to the public search card data.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://www.zillow.com/greenville-sc/"
    }
  ],
  "addresses": [
    "18 Zelma Dr, Greenville, SC 29617"
  ],
  "propertyStatus": "ANY",
  "maxSearchPages": 1,
  "maxItems": 100,
  "includePhotos": true,
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "enrichSearchResults": true
}
```

# Actor output Schema

## `properties` (type: `string`):

Public property records in 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 = {
    "startUrls": [
        {
            "url": "https://www.zillow.com/greenville-sc/"
        }
    ],
    "addresses": [
        "18 Zelma Dr, Greenville, SC 29617"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("mlg14/zillow-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 = {
    "startUrls": [{ "url": "https://www.zillow.com/greenville-sc/" }],
    "addresses": ["18 Zelma Dr, Greenville, SC 29617"],
}

# Run the Actor and wait for it to finish
run = client.actor("mlg14/zillow-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 '{
  "startUrls": [
    {
      "url": "https://www.zillow.com/greenville-sc/"
    }
  ],
  "addresses": [
    "18 Zelma Dr, Greenville, SC 29617"
  ]
}' |
apify call mlg14/zillow-scraper --silent --output-dataset

```

## MCP server setup

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