# Zillow Property Details Scraper - Zestimate, Price, Tax History (`lukehunter/zillow-property-details`) Actor

Get full Zillow property details for URLs or zpids: price, Zestimate, rent Zestimate, beds, baths, area, price history, tax history, schools, brokerage. Change mode sends price, status or Zestimate change alerts to Discord, Telegram, Slack or any webhook. No agent contact data.

- **URL**: https://apify.com/lukehunter/zillow-property-details.md
- **Developed by:** [Luke Hunter](https://apify.com/lukehunter) (community)
- **Categories:** Real estate, Lead generation, Automation
- **Stats:** 1 total users, 1 monthly users, 76.9% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$3.00 / 1,000 properties

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 Property Details Scraper - Zestimate, Price, Tax History

**For real-estate investors, agents and brokerages, proptech teams and lenders' data pipelines, this Actor takes a list of Zillow property URLs or zpids and returns one clean row per property: address, status, price, Zestimate, rent Zestimate, beds, baths, area, price history, tax history, schools and brokerage.**

Pay per delivered property: **$0.003 per property — 1,000 properties = $3.** No start fee. A property that is blocked, missing or (in monitoring mode) unchanged is not charged.

It is built for the way property data is actually used in a pipeline: you pass a list, you get one row per input back, the field names never change, and anything Zillow does not publish is `null` instead of a guess. Call it from code, from a schedule, or from Make, Zapier and n8n.

**At a glance**

- **Paste:** Zillow property URLs (`zillow.com/homedetails/...`) or zpids.
- **Get, per property:** `{"zpid": "20522136", "address": "1001 N Crescent Dr, Beverly Hills, CA 90210", "price": 20995000, "zestimate": 18738900, "bedrooms": 7}`, plus price history, tax history, schools and `brokerageName`.
- **Price:** $0.003 per delivered property (1,000 properties = $3). Free: blocked, missing, invalid, robots-skipped and (in monitoring mode) unchanged inputs.
- **From code and pipelines:** [API, Make, Zapier and n8n](#use-it-from-your-code-make-zapier-or-n8n), or as a tool for AI agents ([MCP](#use-with-ai-agents-mcp)). What it does not do (US only, no agent contact data): [Limitations](#limitations).

### Quick start

Paste property URLs (or zpids), keep the defaults, run:

```json
{
  "urls": [
    "https://www.zillow.com/homedetails/1003-Juanita-St-Austin-TX-78704/29474600_zpid/",
    "https://www.zillow.com/homedetails/1001-N-Crescent-Dr-Beverly-Hills-CA-90210/20522136_zpid/"
  ],
  "zpids": [2131539623],
  "maxItems": 100
}
```

Open the **Properties** dataset view, or export JSON, CSV or Excel. Every input's result (delivered, blocked, not found, skipped...) is also written to the run's `OUTCOMES` record so you can join it to your input list.

### Input

| Field | Type | Default | Description |
|---|---|---:|---|
| `urls` | string\[] | – | Zillow property page URLs (`zillow.com/homedetails/...`). Search pages are rejected with a clear message. |
| `zpids` | string\[] / number\[] | – | Zillow property IDs, e.g. `29474600`. Use instead of, or alongside, `urls`. |
| `maxItems` | integer | 100 | 1–5000. Hard cap on delivered rows, and your cost cap. |
| `includePriceHistory` | boolean | `true` | Add the price history list to each row. |
| `includeTaxHistory` | boolean | `true` | Add the tax history list to each row. |
| `includeSchools` | boolean | `true` | Add the schools list to each row. |
| `onlyChangedSinceLastRun` | boolean | `false` | Monitoring mode: deliver a property only when its price, status or Zestimate changed since the last run. |
| `stateStoreName` | string | `zillow-property-details-state` | Name of the store that remembers each property between monitoring runs. |
| `discordWebhookUrl` | string (secret) | *(none)* | Discord webhook URL. Must look like `https://discord.com/api/webhooks/<id>/<token>`. Empty means no Discord messages. |
| `telegramBotToken` | string (secret) | *(none)* | Telegram bot token from @BotFather. Needs `telegramChatId`. |
| `telegramChatId` | string | *(none)* | Telegram chat to post to: a numeric chat id or `@channelusername`. The bot must be added to that chat or channel. Needs `telegramBotToken`. |
| `slackWebhookUrl` | string (secret) | *(none)* | Slack incoming webhook, `https://hooks.slack.com/services/...`. |
| `slackCompatibleWebhookUrl` | string (secret) | *(none)* | Mattermost / Rocket.Chat incoming webhook (public `https://` URL; Markdown list, no Block Kit). |
| `teamsWebhookUrl` | string (secret) | *(none)* | Microsoft Teams Workflows webhook (host ending `.logic.azure.com` or `.powerplatform.com`). |
| `googleChatWebhookUrl` | string (secret) | *(none)* | Google Chat space webhook, `https://chat.googleapis.com/v1/spaces/...`. |
| `ntfyTopic` | string (secret) | *(none)* | ntfy topic name; setting it turns ntfy phone pushes on. Pick a long, unguessable name. |
| `ntfyServer` | string | `https://ntfy.sh` | ntfy server. Only change it for your own public `https://` ntfy server. |
| `ntfyToken` | string (secret) | *(none)* | Optional ntfy access token for a self-hosted server or protected topic. Needs `ntfyTopic`. |
| `pushoverUserKey` | string (secret) | *(none)* | Pushover user key. Needs `pushoverAppToken`. |
| `pushoverAppToken` | string (secret) | *(none)* | Pushover application API token. Needs `pushoverUserKey`. |
| `webhookUrl` | string (secret) | *(none)* | Generic JSON webhook (Zapier, Make, n8n, Pipedream, IFTTT, Home Assistant): the changed rows, 100 per POST. Public `https://` only. |
| `maxNotifications` | integer | 25 | 1–100. Most property changes sent to chat and phone channels per run (the webhook is not capped). Extra rows are still in the dataset. Only used when a destination is set. |

At least one URL or zpid is required; up to 5,000 per run. A property given twice (as a URL and as a zpid, say) is fetched once. The three `include...` switches only shorten the row; the price is the same.

### What you get per property

| Category | Fields |
|---|---|
| Identity | `zpid`, `url`, `inputUrl` |
| Address | `address`, `addressStreet`, `addressCity`, `addressState`, `addressZipcode`, `latitude`, `longitude` |
| Status and type | `homeStatus`, `homeType` |
| Price and value | `price`, `currency`, `zestimate`, `rentZestimate`, `lastSoldPrice`, `soldDate` |
| Home facts | `bedrooms`, `bathrooms`, `livingAreaSqft`, `lotSizeSqft`, `yearBuilt`, `daysOnZillow` |
| Interest | `pageViewCount`, `favoriteCount` (when Zillow shows them) |
| Costs and tax | `hoaFee`, `propertyTaxRate`, `taxAssessedValue` |
| History | `priceHistory` (`date`, `event`, `price`), `taxHistory` (`year`, `taxPaid`, `value`) |
| Neighbourhood | `schools` (`name`, `rating`, `distance`, `level`) |
| Listing | `description`, `brokerageName`, `imageUrls` (first 10) |
| Freshness | `scrapedAt` |

Anything Zillow does not publish is `null` (or an empty list for histories), never an estimate of ours. Zillow sends a price of 0 for off-market homes; that comes back as `null`, not `$0`.

### Example output

A real row, parsed from a live Zillow page (a for-sale house in Beverly Hills); the description, price history, tax history, schools and photo lists are shortened here for readability, and the actual row carries them in full:

```json
{
  "zpid": "20522136",
  "url": "https://www.zillow.com/homedetails/1001-N-Crescent-Dr-Beverly-Hills-CA-90210/20522136_zpid/",
  "address": "1001 N Crescent Dr, Beverly Hills, CA 90210",
  "addressStreet": "1001 N Crescent Dr",
  "addressCity": "Beverly Hills",
  "addressState": "CA",
  "addressZipcode": "90210",
  "latitude": 34.08441,
  "longitude": -118.414795,
  "homeStatus": "FOR_SALE",
  "homeType": "SINGLE_FAMILY",
  "price": 20995000,
  "currency": "USD",
  "zestimate": 18738900,
  "rentZestimate": null,
  "bedrooms": 7,
  "bathrooms": 8,
  "livingAreaSqft": 7585,
  "lotSizeSqft": 40902,
  "yearBuilt": 1955,
  "daysOnZillow": 98,
  "pageViewCount": 1384,
  "favoriteCount": 107,
  "lastSoldPrice": 18550000,
  "soldDate": null,
  "description": "Set behind double gates in one of the most coveted pockets of Beverly Hills - North of Sunset, just behind the iconic Beverly Hills Hotel - this extraordinary estate offers both pedigree and privacy ...",
  "hoaFee": null,
  "propertyTaxRate": 1.18,
  "taxAssessedValue": 18921000,
  "priceHistory": [
    { "date": "2026-06-26", "event": "Price change", "price": 20995000 },
    { "date": "2025-06-17", "event": "Listed for sale", "price": 22495000 }
  ],
  "taxHistory": [
    { "year": 2026, "taxPaid": null, "value": 18921000 },
    { "year": 2025, "taxPaid": 223340.81, "value": 18550000 }
  ],
  "schools": [
    { "name": "El Rodeo Elementary School", "rating": 10, "distance": 1.1, "level": "Elementary" },
    { "name": "Beverly Hills High School", "rating": 9, "distance": 1.6, "level": "High" }
  ],
  "brokerageName": "Carolwood Estates",
  "imageUrls": [
    "https://photos.zillowstatic.com/fp/a3328bfc7f987dc46ffc910291ea65c3-uncropped_scaled_within_1536_1152.jpg",
    "https://photos.zillowstatic.com/fp/5879ddf38c80f2d0f8c0caa2ca10bf58-uncropped_scaled_within_1536_1152.jpg"
  ],
  "scrapedAt": "2026-10-03T00:00:00.000Z",
  "inputUrl": "https://www.zillow.com/homedetails/1001-N-Crescent-Dr-Beverly-Hills-CA-90210/20522136_zpid/"
}
```

### Use cases

- **Investors:** pull the Zestimate, tax-assessed value, tax history and price-cut history for every property on a deal list, and rank them in a spreadsheet.
- **Agents and brokerages:** enrich a CRM or comps sheet with live status, days on Zillow, beds, baths and area for the addresses you already track.
- **Proptech products:** feed an app or valuation model with a stable, flat schema instead of parsing Zillow pages yourself.
- **Lenders and servicers:** refresh collateral data (status, price, Zestimate, assessed value, tax history) for a loan or portfolio list on a schedule.

### Use it from your code

Pass your list, get rows back. JavaScript:

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

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });

const run = await client.actor('lukehunter/zillow-property-details').call({
    zpids: [29474600, 20522136, 2131539623],
    includeSchools: false,
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const home of items) {
    console.log(home.zpid, home.homeStatus, home.price, home.zestimate);
}

// Which inputs produced a row, and why not for the others:
const outcomes = await client.keyValueStore(run.defaultKeyValueStoreId).getRecord('OUTCOMES');
```

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")

run = client.actor("lukehunter/zillow-property-details").call(
    run_input={"zpids": [29474600, 20522136, 2131539623], "includeSchools": False}
)

for home in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(home["zpid"], home["homeStatus"], home["price"], home["zestimate"])

outcomes = client.key_value_store(run["defaultKeyValueStoreId"]).get_record("OUTCOMES")["value"]
```

Or call the HTTP API directly: one request runs the Actor and returns the rows (best for lists of up to about 80 properties, because the synchronous endpoint waits at most five minutes):

```bash
curl -X POST "https://api.apify.com/v2/acts/lukehunter~zillow-property-details/run-sync-get-dataset-items?token=YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"zpids": [29474600, 20522136], "maxItems": 2}'
```

For longer lists start an asynchronous run (`/acts/lukehunter~zillow-property-details/runs`) and read the dataset when it finishes. The same Actor works in Make, Zapier and n8n through the Apify integrations. The `OUTCOMES` record lists one entry per input in the order you gave them: `input`, `zpid`, `status` (`ok`, `unchanged`, `blocked`, `rate-limited`, `not-found`, `skipped-robots`, `invalid-input`, `duplicate`, `failed`, `limit-reached` or `not-attempted`) and a plain-English `detail` for anything that did not produce a row.

### Monitor properties for changes

Run the same list on a schedule and only pay for what moved:

1. Create an Apify **Schedule** (daily, say) for a task with your `urls` / `zpids` and `"onlyChangedSinceLastRun": true`.
2. The first run delivers every property and remembers its price, status and Zestimate.
3. Every later run delivers a row only when one of those three changed (or the property is new to the list). Unchanged properties are skipped and not charged. Photos, descriptions or page views changing never trigger a row.
4. Add a **webhook** on run success (or use an integration) to push the new rows to Slack, a database or your CRM.

```json
{
  "zpids": [29474600, 20522136, 2131539623],
  "onlyChangedSinceLastRun": true,
  "stateStoreName": "my-deal-list"
}
```

Use a different `stateStoreName` for each watch list that should keep its own history. A property blocked on one run keeps its previous state and is compared again next time.

### Get Zillow alerts in Discord, Telegram, Slack, Teams, Google Chat, your phone (ntfy/Pushover) or any app (webhook)

Want a **Zillow property change alert** in Discord, Telegram, Slack, Teams, Google Chat or on your phone? Turn on monitoring mode (`onlyChangedSinceLastRun`), add one or more destinations in the Actor's **Notifications (optional)** section and each run sends the properties whose price, status or Zestimate changed. It is a **monitor that notifies you**: it only reads public Zillow property pages and never contacts agents or owners, buys anything or logs in anywhere.

| Channel | Fill in | What you get |
|---|---|---|
| Discord | `discordWebhookUrl` | One card per property change, 10 per message |
| Telegram | `telegramBotToken` + `telegramChatId` | Up to 10 property changes per message, one line each |
| Slack | `slackWebhookUrl` | Block Kit message, up to 10 property changes per message |
| Mattermost, Rocket.Chat | `slackCompatibleWebhookUrl` | Slack-style incoming webhook: a Markdown list (these apps do not render Slack Block Kit) |
| Microsoft Teams | `teamsWebhookUrl` | Adaptive Card from a Teams Workflows webhook, up to 10 property changes per card |
| Google Chat | `googleChatWebhookUrl` | Card in a space, up to 10 property changes per message |
| ntfy (phone push, no account) | `ntfyTopic` (optional `ntfyServer`, `ntfyToken`) | A push per property change, up to 10, then one "...and N more" |
| Pushover (phone push) | `pushoverUserKey` + `pushoverAppToken` | A push per property change, up to 10, then one "...and N more" |
| Any app: Zapier, Make, n8n, Pipedream, IFTTT, Home Assistant | `webhookUrl` | One JSON POST per run with the changed rows |

Fill in as many as you like; each channel works on its own and a failing one never stops the others.

#### Setup, channel by channel

- **Discord (1 minute):** Server Settings → Integrations → Webhooks → New Webhook → pick the channel → **Copy Webhook URL**.
- **Telegram (2 minutes):** chat with **@BotFather**, send `/newbot`, paste the token into `telegramBotToken`. Add the bot to the chat or channel (administrator for a channel; for a chat with yourself press **Start** on the bot). `telegramChatId` is a numeric id (`123456789`, or `-1001234567890` for a group or channel) or a public `@channelusername`.
- **Slack:** at api.slack.com/apps create an app (**From scratch**) → **Incoming Webhooks** → switch it on → **Add New Webhook to Workspace** → pick the channel → copy the `https://hooks.slack.com/services/...` URL.
- **Mattermost or Rocket.Chat:** create an **incoming webhook** in the app and paste its public `https://` URL into `slackCompatibleWebhookUrl`. Localhost, private IP addresses and `.local` / `.internal` names are rejected.
- **Microsoft Teams:** in the channel open **... → Workflows** → **Post to a channel when a webhook request is received** → finish the wizard and copy the URL (a host ending in `.logic.azure.com` or `.powerplatform.com`). The old "Office 365 connector" webhooks are retired and not supported.
- **Google Chat:** in the space open **Apps & integrations → Webhooks → Add webhook**, name it and copy the `https://chat.googleapis.com/v1/spaces/...` URL (needs a Google Workspace account that allows webhooks).
- **ntfy (free, no account):** install the ntfy app (or open ntfy.sh), subscribe to a topic with a long random name, and put the same name in `ntfyTopic`. Anyone who knows the topic can read it, so make it unguessable. Own server or a protected topic: set `ntfyServer` (public `https://` address) and `ntfyToken`.
- **Pushover:** copy your user key from pushover.net into `pushoverUserKey`, create an application at pushover.net/apps/build and put its API token in `pushoverAppToken`. Pushover itself is a separate paid app after its trial: check their pricing.
- **Any app (webhook):** paste the catch-hook URL from Zapier, Make, n8n, Pipedream, IFTTT or Home Assistant into `webhookUrl`. It must be a public `https://` address.

#### The recipe: a Zillow property watch in five steps

1. List the properties to watch in `urls` and/or `zpids`.
2. Turn on `onlyChangedSinceLastRun`. Alerts only ever cover changes, so they need monitoring mode.
3. Add your destination(s) from the table above.
4. Run it once. The first run is the baseline: it delivers and remembers every property and sends nothing.
5. Create a **Schedule** (for example daily). From then on you hear about a price, status or Zestimate change.

```json
{
  "zpids": [
    "20522136"
  ],
  "onlyChangedSinceLastRun": true,
  "maxItems": 100,
  "ntfyTopic": "your-long-random-topic-name",
  "maxNotifications": 25
}
```

#### The generic webhook (`webhookUrl`)

Each run sends **one POST per 100 changed rows** (so usually one) with this body, followed by whatever your tool does with it:

```json
{
  "source": "apify",
  "actor": "zillow-property-details",
  "runId": "HG7ML7M8z78YcAPEB",
  "count": 2,
  "total": 2,
  "part": 1,
  "parts": 1,
  "items": [ { "...": "the full dataset rows" } ]
}
```

`count` is the number of rows in this POST, `total` the number in the run, and `part` / `parts` number the POSTs when a run delivers more than 100 rows. `maxNotifications` does not cap the webhook. Your endpoint should answer with a 2xx status; redirects are not followed and a failure is reported in the run status, never retried in a loop.

#### What to expect

- **What a message contains:** what the property looks like now: the price, status, Zestimate, bedrooms, bathrooms, size, days on Zillow, home type, a photo and the link.
- **Only changes are sent.** The first run (the baseline) and a normal run without monitoring mode deliver every property rather than changes, so they send nothing and the status message says why. A run where nothing changed sends nothing.
- **An alert shows the property as it is now, not "was X".** Monitoring remembers a fingerprint of price, status and Zestimate, not the old values, so an alert names the new ones rather than the previous ones.
- **`maxNotifications` (default 25, max 100)** caps how many property changes go to chat and phone channels per run (the webhook is not capped). The rest are still in the dataset and the status message says so. ntfy and Pushover send at most 10 individual pushes, then one "...and N more" summary.
- **Cost: notifications are free.** You pay only for delivered properties at the usual price, exactly as without alerts. There is no extra charge for any channel.
- **Failures never fail the run.** A deleted webhook or an unreachable service leaves the run successful and the dataset complete, and the status message says what happened, for example `Notified: Discord 12, Slack 12, ntfy 10 (+summary). Teams failed (HTTP 404) - check the webhook URL.` The logs show the channel name and HTTP status only, never a URL or token.
- **Your URLs, tokens and ntfy topic are stored as secret inputs.** Anyone holding a webhook URL can post to that channel, so treat it like a password.
- **The dataset is unchanged.** The output rows are identical whether or not you set a destination; the run's status message just gains a short note such as `Notified: Discord 12, ntfy 10 (+summary).`
- **It is a monitor that notifies you.** It never contacts agents or owners, buys anything or logs in to Zillow, and the messages go only to the destinations you set up.

### Use with AI agents (MCP)

This Actor works as a tool for Claude, ChatGPT, Cursor and other MCP clients through [Apify's MCP server](https://mcp.apify.com). A call needs only `urls` (Zillow property links) or `zpids`.

**Claude Desktop** (`claude_desktop_config.json`), with your own token from Apify Console > Settings > API & Integrations:

```json
{
  "mcpServers": {
    "apify-zillow": {
      "command": "npx",
      "args": [
        "-y", "mcp-remote",
        "https://mcp.apify.com?tools=lukehunter/zillow-property-details",
        "--header", "Authorization: Bearer ${APIFY_TOKEN}"
      ],
      "env": { "APIFY_TOKEN": "<YOUR_APIFY_TOKEN>" }
    }
  }
}
```

Clients that support OAuth (Claude.ai, VS Code) can connect with just the URL `https://mcp.apify.com?tools=lukehunter/zillow-property-details` and sign in to Apify. Never paste a real token into a shared file or chat.

**Example.** You ask: "What's the Zestimate and last sale price for zpid 29474600?" The agent calls the tool `lukehunter--zillow-property-details` with:

```json
{
  "zpids": [29474600],
  "includePriceHistory": true,
  "maxItems": 5
}
```

It is charged exactly like any run: per delivered property, nothing for properties that are blocked, not found or skipped. Set `maxItems` to cap spend.

### Pricing and cost control

This Actor uses **pay per delivered property** pricing: **$0.003 per property**, no start fee.

| Properties delivered | Cost at $0.003/property |
|---:|---:|
| 1 | $0.003 |
| 100 | $0.30 |
| 1,000 | $3.00 |
| 5,000 | $15.00 |

`maxItems` and your run's maximum cost limit both cap spending: each property is charged right after it is delivered, and the Actor stops requesting pages once the limit cannot pay for another one. Blocked, missing, invalid, robots-skipped and unchanged inputs cost nothing.

Notifications to Discord, Telegram, Slack, Teams, Google Chat, ntfy, Pushover and webhooks are free: you pay only for delivered properties, never for messages sent.

### Reliability and data quality

- One row per delivered property with the same fields every time; missing values are `null`, never guessed.
- A property is checked to be the one you asked for: a page that turns out to be a different property, a search page or an off-site redirect is discarded unread.
- One bad input never sinks a run. Each URL or zpid gets its own outcome and the rest continue.
- If Zillow blocks or rate-limits requests, that is reported per input and nothing is charged. This Actor does not use proxies, CAPTCHA solving, cookies, logins or User-Agent rotation. After 10 blocked responses in a row it stops asking and marks the remaining inputs `not-attempted` so you can re-run them later.
- It identifies itself honestly, reads Zillow's `robots.txt` on every run, and makes about one request every three seconds.

#### Changelog

- **2026-10-10:** accepts every common way of pasting a property: mobile links (`m.zillow.com`), links without `https://`, short `/homes/<zpid>_zpid/` links, bare zpids and pasted lists. Inputs that are not property pages (search pages, other sites) no longer fail the run: they are skipped free, with a message saying what works. The run's success rate on the Store page includes failures from before this fix; they age out of its 30-day window.

### Limitations

- US properties only (Zillow's coverage), prices in USD.
- About one request every 3 seconds, so roughly 20 properties per minute at most. A single property returns in seconds.
- Up to 5,000 URLs and zpids and 5,000 delivered rows per run.
- Zillow's `robots.txt` disallows some `/homedetails/` variants (`?view=owner`, `?altId=...`); those inputs are skipped and reported as `skipped-robots`.
- Only single property pages (`zillow.com/homedetails/...`) are read. For search results by ZIP code use the Zillow ZIP Code Scraper.
- Individual listing agents' names, phone numbers, licence numbers and e-mail addresses are never collected. `brokerageName` is the brokerage business. E-mail addresses and phone numbers typed into a listing description are replaced with `[email removed]` / `[phone removed]`.
- `hoaFee` is filled only when Zillow publishes it as a plain number or dollar amount; `pageViewCount` and `favoriteCount` only when the page shows them; `price` is `null` for off-market homes.
- Every row is a point-in-time observation identified by `scrapedAt`; Zillow data changes continuously.
- This is an independent tool and is **not affiliated with, endorsed by, or connected to Zillow Group, Inc.** Zillow is a trademark of its owner. You are responsible for using the data in accordance with applicable terms and laws.

### Use it from your code, Make, Zapier or n8n

Built for automated pipelines: small runs, stable field names, one row per item, and you pay only for rows actually delivered.

**One API call that runs the Actor and returns the data** (replace `$APIFY_TOKEN` with your token from Apify Console → Settings → Integrations):

```bash
curl -s -X POST "https://api.apify.com/v2/acts/lukehunter~zillow-property-details/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"urls": ["https://www.zillow.com/homedetails/1003-Juanita-St-Austin-TX-78704/29474600_zpid/", "https://www.zillow.com/homedetails/1001-N-Crescent-Dr-Beverly-Hills-CA-90210/20522136_zpid/", "https://www.zillow.com/homedetails/100-W-27th-St-APT-5S-New-York-NY-10001/2131539623_zpid/"], "maxItems": 10}'
```

**JavaScript** (`npm i apify-client`):

```js
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('lukehunter~zillow-property-details').call({
  "urls": [
    "https://www.zillow.com/homedetails/1003-Juanita-St-Austin-TX-78704/29474600_zpid/",
    "https://www.zillow.com/homedetails/1001-N-Crescent-Dr-Beverly-Hills-CA-90210/20522136_zpid/",
    "https://www.zillow.com/homedetails/100-W-27th-St-APT-5S-New-York-NY-10001/2131539623_zpid/"
  ],
  "maxItems": 10
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

**Python** (`pip install apify-client`):

```python
import os
from apify_client import ApifyClient
client = ApifyClient(os.environ['APIFY_TOKEN'])
run = client.actor('lukehunter~zillow-property-details').call(run_input={"urls": ["https://www.zillow.com/homedetails/1003-Juanita-St-Austin-TX-78704/29474600_zpid/", "https://www.zillow.com/homedetails/1001-N-Crescent-Dr-Beverly-Hills-CA-90210/20522136_zpid/", "https://www.zillow.com/homedetails/100-W-27th-St-APT-5S-New-York-NY-10001/2131539623_zpid/"], "maxItems": 10})
items = client.dataset(run['defaultDatasetId']).list_items().items
```

**Schedules, webhooks and no-code tools:** save your input as a Task, add a Schedule (e.g. hourly or daily), and attach a webhook or one of the Integrations tab's connectors (Make, Zapier, n8n, Google Sheets, Slack) to push each run's results into your own system.

**Only new items each run:** set `"onlyChangedSinceLastRun": true` on a scheduled Task and every run after the first delivers, and charges for, only items it hasn't delivered before for that same input. That makes it a ready-made change feed.

**Reliability:** an automated test runs this Actor's parser against the live site every day, and failures are fixed fast.

**Feedback:** an honest rating on the Store page, good or bad, helps others decide if this Actor fits their needs. Missing a field or found a problem? Open an issue on the Actor page.

### FAQ

#### Can I use just zpids?

Yes. A zpid is the number in a Zillow property URL (`.../29474600_zpid/`). The Actor requests `zillow.com/homedetails/<zpid>_zpid/`; if Zillow does not resolve a zpid to a property page, that input is reported as `not-found` or `failed` in `OUTCOMES` and nothing is charged.

#### Why is `price` null on some properties?

Zillow shows no asking price for off-market and recently sold homes (it sends 0 internally). The Actor returns `null` rather than `$0`. Use `lastSoldPrice` and `soldDate` when Zillow publishes them, and `zestimate` for its own estimate.

#### Why is `zestimate` null on a rental?

Zillow publishes a rent Zestimate for most rentals instead; see `rentZestimate`.

#### What does a "Price change" in `priceHistory` mean?

`priceHistory` reproduces Zillow's own event list: `date`, `event` (for example "Listed for sale", "Price change", "Sold", "Listing removed") and the `price` at that event, newest first. Some events, such as a sale in a non-disclosure state, have no price and return `null`.

#### Does it collect agent or owner contact details?

No. Only listing and property fields are returned. See the Limitations section.

#### Can I look up properties by address or ZIP code?

Not with this Actor: it needs a property URL or zpid. Use the Zillow ZIP Code Scraper to find properties by ZIP, then pass their `zpid` values here.

#### Do I need a Zillow account or API key?

No. The Actor reads public property pages as a signed-out visitor and stops if Zillow serves a block or challenge page.

#### Can I get Zillow alerts by email?

Not natively. There are two routes. (1) Set `webhookUrl` to a Zapier, Make or n8n catch hook and add an email or Gmail step: the webhook delivers the property changes as JSON, so the email can list them. (2) Apify's own run integrations and run notifications can email you when a run finishes, but they only send a link to the run or dataset, not the property changes themselves. For instant alerts without building anything, Telegram, ntfy or Pushover reach your phone directly.

#### Can I get Zillow alerts in WhatsApp?

Not natively, and we do not use unofficial WhatsApp APIs, which risk getting your number banned. The official route is the WhatsApp Business Platform (Cloud API) with its own business account, message templates and pricing: point `webhookUrl` at a Zapier, Make or n8n flow that calls it. For free phone alerts use Telegram or ntfy instead.

### Related Actors

Other data tools from the same developer, built to the same standard: official or public sources, hard cost caps, and honest documentation of limits.

- **[Zillow Price Drop & New Listing Monitor](https://apify.com/lukehunter/zillow-price-drop-monitor)**: stateful ZIP-code change events for Zillow: new listings, price cuts and removed listings, run on a schedule.
- **[Rightmove Property Scraper](https://apify.com/lukehunter/rightmove-scraper)**: UK property listings from Rightmove search results, with price, address, bedrooms and agent details.
- **[Rightmove Property Details Scraper](https://apify.com/lukehunter/rightmove-property-details)**: full UK property records by URL or ID: price, status, features, history, with a change-only mode.
- **[UK Tenders Monitor](https://apify.com/lukehunter/uk-tenders-monitor)**: new UK public-sector tenders from Contracts Finder and Find a Tender, filtered by CPV code, value, region and buyer, with Slack, Discord and Telegram alerts.
- **[TED EU Tenders Monitor](https://apify.com/lukehunter/ted-eu-tenders-monitor)**: new EU public tenders, awards and prior information notices from TED (Tenders Electronic Daily), filtered by CPV code, buyer country, value and keyword, with Slack, Discord and Telegram alerts.
- **[USAspending Federal Contract Awards Monitor](https://apify.com/lukehunter/usaspending-federal-awards-monitor)**: US federal contract awards from the official USAspending API: winners, amounts, agencies and NAICS codes, with new-award alerts to Slack, Discord and Telegram.
- **[Bank Branch Closings & Merger Radar (FDIC)](https://apify.com/lukehunter/bank-branch-merger-radar)**: new US bank branch closings, openings and mergers from the FDIC.
- **[Chicago New Business License Radar](https://apify.com/lukehunter/chicago-new-business-license-radar)**: newly licensed Chicago businesses as opening leads, grouped by category.

# Actor input Schema

## `urls` (type: `array`):

Zillow property page URLs, e.g. "https://www.zillow.com/homedetails/1003-Juanita-St-Austin-TX-78704/29474600_zpid/". Also accepted: m.zillow.com links, zillow.com/homes/<zpid>\_zpid/ links, bare zpid numbers, and pasted lists separated by commas or new lines. Search, building and rental pages are not single properties: they are reported per item (free) and skipped. Up to 5,000 URLs and zpids per run. One row is delivered per property.

## `zpids` (type: `array`):

Zillow property IDs, e.g. "29474600" (the number before \_zpid in a property URL). Use this instead of, or alongside, "urls" when your data already has zpids. A property given twice is fetched once.

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

Hard cap on the number of property rows delivered in the run, 1-5000, default 100. You are charged $0.003 per delivered property, so this is your cost cap.

## `includePriceHistory` (type: `boolean`):

Add the property's price history (listed, price change, sold, listing removed events with dates and prices) to each row. Turn off for a leaner row; the price is the same either way.

## `includeTaxHistory` (type: `boolean`):

Add the property's tax history (year, tax paid, assessed value) to each row. Turn off for a leaner row; the price is the same either way.

## `includeSchools` (type: `boolean`):

Add the schools Zillow lists for the property (name, GreatSchools rating, distance in miles, level) to each row. Turn off for a leaner row; the price is the same either way.

## `onlyChangedSinceLastRun` (type: `boolean`):

Turn this on and run the Actor on a schedule to get a row only when a property's price, status or Zestimate changed since the last run (or it is seen for the first time). Unchanged properties are skipped and never charged. The first run delivers every property and remembers it. Off by default, so a plain run always delivers every property.

## `stateStoreName` (type: `string`):

Only used when monitoring is on. Name of the key-value store that remembers each property's last price, status and Zestimate between runs. Leave as the default unless several tasks must not share their history. Letters, numbers and hyphens only.

## `discordWebhookUrl` (type: `string`):

Post results to a Discord channel. In Discord: Server Settings -> Integrations -> Webhooks -> New Webhook -> choose the channel -> Copy Webhook URL. Example: https://discord.com/api/webhooks/<id>/<token>. Leave empty to skip Discord.

## `telegramBotToken` (type: `string`):

Post results to Telegram. Create a bot by chatting with @BotFather on Telegram (send /newbot) and paste the token it gives you, e.g. 123456789:AA.... Needs "Telegram chat ID" below. Leave empty to skip Telegram.

## `telegramChatId` (type: `string`):

Where the bot should post: a numeric chat id (e.g. 123456789, or -1001234567890 for a group or channel) or a public @channelusername. The bot must be added to that chat or channel first (as an administrator for a channel), and for a private chat you must press Start on the bot. Needs "Telegram bot token" above.

## `slackWebhookUrl` (type: `string`):

Post results to a Slack channel (Block Kit cards, up to 10 per message). Create an incoming webhook at api.slack.com/apps -> Create New App -> Incoming Webhooks -> Add New Webhook to Workspace. Example: https://hooks.slack.com/services/T000/B000/XXXX. Leave empty to skip Slack.

## `slackCompatibleWebhookUrl` (type: `string`):

Post results to a Mattermost or Rocket.Chat incoming webhook, which accept Slack-style webhook payloads. Must be a public https:// address, e.g. https://chat.example.com/hooks/abc123 (no localhost, private IP or .local/.internal hosts). These apps do not render Slack Block Kit, so they get a plain Markdown list. For real Slack use "Slack webhook URL" above. Leave empty to skip.

## `teamsWebhookUrl` (type: `string`):

Post results to a Teams channel as an Adaptive Card. In Teams: channel -> ... -> Workflows -> "Post to a channel when a webhook request is received" -> copy the URL. The host must end in .logic.azure.com or .powerplatform.com, e.g. https://prod-12.westeurope.logic.azure.com/workflows/... The retired Office 365 connector webhooks are not supported. Leave empty to skip Teams.

## `googleChatWebhookUrl` (type: `string`):

Post results to a Google Chat space. In the space: Apps & integrations -> Webhooks -> Add webhook -> copy the URL. Example: https://chat.googleapis.com/v1/spaces/<space>/messages?key=...\&token=.... Leave empty to skip Google Chat.

## `ntfyTopic` (type: `string`):

Get a phone push with no account via ntfy (ntfy.sh). Install the ntfy app, subscribe to a topic name, and enter the same name here. Anyone who knows the topic can read it, so pick a long, unguessable name (letters, numbers, - and \_), e.g. acme-leads-7k2x9q. At most 10 individual pushes per run, then one "...and N more". Leave empty to skip ntfy.

## `ntfyServer` (type: `string`):

Only used with "ntfy topic". Leave as https://ntfy.sh, or enter the public https:// address of your own ntfy server, e.g. https://ntfy.example.com.

## `ntfyToken` (type: `string`):

Only used with "ntfy topic", for a self-hosted server or a reserved topic that needs a login. An access token from ntfy (starts with tk\_, e.g. tk_abc123...). Leave empty for open topics.

## `pushoverUserKey` (type: `string`):

Get a phone push via Pushover. Your user key (30 characters, e.g. uQiRzpo4DXghDmr9QzzfQu27cmVRsG) is shown on the Pushover dashboard (pushover.net). Needs "Pushover app token" below. At most 10 individual pushes per run, then one "...and N more". Leave empty to skip Pushover.

## `pushoverAppToken` (type: `string`):

The API token of a Pushover application you create at pushover.net/apps/build (name it after this Actor), e.g. azGDORePK8gMaC0QOYAMyEEuzJnyUi. Needs "Pushover user key" above.

## `webhookUrl` (type: `string`):

POST the delivered rows as JSON to any public https:// URL: {source, actor, runId, count, items}. At most 100 rows per POST; every delivered row is sent regardless of the max-notifications cap. For example, route results to email, a spreadsheet or any app. Localhost, private IP and .local/.internal addresses are rejected. Leave empty to skip.

## `maxNotifications` (type: `integer`):

Caps how many results are sent to chat and phone channels per run (Discord, Telegram, Slack, Teams, Google Chat, ntfy, Pushover), 1-100, default 25 (e.g. 10). Extra results are still in the dataset. The generic webhook is not capped. Only used when a destination above is set.

## Actor input object example

```json
{
  "urls": [
    "https://www.zillow.com/homedetails/1003-Juanita-St-Austin-TX-78704/29474600_zpid/",
    "https://www.zillow.com/homedetails/1001-N-Crescent-Dr-Beverly-Hills-CA-90210/20522136_zpid/",
    "https://www.zillow.com/homedetails/100-W-27th-St-APT-5S-New-York-NY-10001/2131539623_zpid/"
  ],
  "maxItems": 10,
  "includePriceHistory": true,
  "includeTaxHistory": true,
  "includeSchools": true,
  "onlyChangedSinceLastRun": false,
  "stateStoreName": "zillow-property-details-state",
  "ntfyServer": "https://ntfy.sh",
  "maxNotifications": 25
}
```

# Actor output Schema

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

No description

# 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 = {
    "urls": [
        "https://www.zillow.com/homedetails/1003-Juanita-St-Austin-TX-78704/29474600_zpid/",
        "https://www.zillow.com/homedetails/1001-N-Crescent-Dr-Beverly-Hills-CA-90210/20522136_zpid/",
        "https://www.zillow.com/homedetails/100-W-27th-St-APT-5S-New-York-NY-10001/2131539623_zpid/"
    ],
    "maxItems": 10
};

// Run the Actor and wait for it to finish
const run = await client.actor("lukehunter/zillow-property-details").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 = {
    "urls": [
        "https://www.zillow.com/homedetails/1003-Juanita-St-Austin-TX-78704/29474600_zpid/",
        "https://www.zillow.com/homedetails/1001-N-Crescent-Dr-Beverly-Hills-CA-90210/20522136_zpid/",
        "https://www.zillow.com/homedetails/100-W-27th-St-APT-5S-New-York-NY-10001/2131539623_zpid/",
    ],
    "maxItems": 10,
}

# Run the Actor and wait for it to finish
run = client.actor("lukehunter/zillow-property-details").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 '{
  "urls": [
    "https://www.zillow.com/homedetails/1003-Juanita-St-Austin-TX-78704/29474600_zpid/",
    "https://www.zillow.com/homedetails/1001-N-Crescent-Dr-Beverly-Hills-CA-90210/20522136_zpid/",
    "https://www.zillow.com/homedetails/100-W-27th-St-APT-5S-New-York-NY-10001/2131539623_zpid/"
  ],
  "maxItems": 10
}' |
apify call lukehunter/zillow-property-details --silent --output-dataset

```

## MCP server setup

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

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/2mJGTcsJc4PoSCvoG/builds/ECOzG7iLgU9gFPUmd/openapi.json
