# Zillow Tax History Scraper (Cheap) (`data_api/zillow-tax-history-scraper-cheap`) Actor

Property price and tax history scraper that outputs one flat row per event. Every sale, price change, and tax year becomes its own dataset record, ready to load into a spreadsheet or database without any post-processing.

- **URL**: https://apify.com/data\_api/zillow-tax-history-scraper-cheap.md
- **Developed by:** [Data API](https://apify.com/data_api) (community)
- **Categories:** Real estate, Automation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.99 / 1,000 results

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

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

## What's an Apify Actor?

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

## How to integrate an Actor?

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

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

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

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

# README

## Zillow Tax History Scraper

![Zillow Tax History Scraper](cover.jpg)

Zillow shows a property's tax bills and past sales in a tidy little widget, but try pulling that into a model and you are stuck copying numbers cell by cell. This actor does the lifting. Hand it a Zillow property ID, a listing link, or a plain street address, and it gives you the full tax and price history for that home as one flat row. Every assessed value, every annual tax bill, and every sale lands in numbered columns you can drop straight into a spreadsheet.

### What you get

One row per property, with two kinds of history laid out side by side so nothing needs reshaping after export:

- **Sale and price events** — for each of up to 10 events: `sale1Date`, `sale1Price`, `sale1PricePerSqft`, `sale1ChangePct`, `sale1EventType`, `sale1ListingSource`, `sale1BuyerAgent`, `sale1SellerAgent`, running through `sale10*`.
- **Tax records** — for each of up to 10 years: `levy1Year`, `levy1TaxPaid`, `levy1TaxChangePct`, `levy1AssessedValue`, `levy1AssessedChangePct`, running through `levy10*`.
- **Lookup context** — `queryInput`, `zillowPropertyId`, `collectedAt`, and `errorMessage` so you can trace every row back to what you asked for.

Every rate from Zillow arrives as a plain percentage, so there are no decimal fractions to convert on your end.

### Quick start

1. Click **Try for free** and open the input form.
2. Fill in at least one of **Zillow property IDs**, **Listing page URLs**, or **Property addresses** — mix all three in a single run if you like.
3. Set **Run timeout (seconds)** to fit the size of your list (about 30 seconds per property).
4. Press **Start**, then export the dataset as JSON, CSV, Excel, or XML when the run finishes.

![How it works](how-it-works.jpg)

### Use cases

- **Deal screening** — pull tax and price history for a shortlist of homes and rank them on tax burden or recent price moves
- **Housing market research** — build a dataset of assessed values and tax trends across a neighborhood or ZIP
- **Comparative market analysis** — line up past sale prices and price-per-square-foot for nearby properties
- **Property tax appeals** — gather year-over-year assessed value and tax changes to support a case
- **Investment underwriting** — feed historical tax bills into cash-flow models without manual entry
- **Data enrichment** — turn a list of addresses or Zillow links you already have into a clean history table

### Input

Provide at least one of the three lookup fields. You can combine IDs, URLs, and addresses freely in one run.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `propertyIds` | array of strings | One of propertyIds, listingPageUrls, or propertyAddresses | Zillow ZPIDs to look up, one per line. Prefilled with `29489015`, `32345923`. |
| `listingPageUrls` | array of strings | One of propertyIds, listingPageUrls, or propertyAddresses | Zillow home detail links, one per line. Works whether or not the ID sits in the path. |
| `propertyAddresses` | array of strings | One of propertyIds, listingPageUrls, or propertyAddresses | Full street addresses, one per line. The actor resolves the matching ID first. |
| `timeoutSeconds` | integer | No | Total run budget in seconds. Raise it for longer lists, around 30 seconds per property. Default `360`. |

#### Example input

```json
{
    "propertyIds": ["29489015", "32345923"],
    "listingPageUrls": [
        "https://www.zillow.com/homedetails/1234-Maple-Grove-Denver-CO-80205/29489015_zpid/"
    ],
    "propertyAddresses": ["1234 Maple Grove Dr, Denver, CO 80205"],
    "timeoutSeconds": 360
}
```

### Output

Each property becomes a single wide row. Up to 10 sale events sit in `sale1*` through `sale10*` columns and up to 10 tax years in `levy1*` through `levy10*`. Columns for events a property does not have are simply left out, so a run of 50 properties gives you 50 rows.

#### Example output

```json
{
    "queryInput": "1234 Maple Grove Dr, Denver, CO 80205",
    "zillowPropertyId": "29489015",
    "sale1Date": "2026-01-18",
    "sale1Price": 845000,
    "sale1PricePerSqft": 312,
    "sale1ChangePct": -2.31,
    "sale1EventType": "Listed for sale",
    "sale1ListingSource": "REcolorado",
    "sale1BuyerAgent": null,
    "sale1SellerAgent": null,
    "sale2Date": "2021-06-09",
    "sale2Price": 612000,
    "sale2PricePerSqft": 226,
    "sale2ChangePct": 9.82,
    "sale2EventType": "Sold",
    "sale2ListingSource": "REcolorado",
    "sale2BuyerAgent": "Maria Delgado",
    "sale2SellerAgent": "Tom Hargrove",
    "sale3Date": "2016-04-22",
    "sale3Price": 478000,
    "sale3PricePerSqft": 177,
    "sale3ChangePct": 0,
    "sale3EventType": "Sold",
    "sale3ListingSource": "REcolorado",
    "sale3BuyerAgent": null,
    "sale3SellerAgent": "Tom Hargrove",
    "levy1Year": 2025,
    "levy1TaxPaid": 6480,
    "levy1TaxChangePct": 4.12,
    "levy1AssessedValue": 689000,
    "levy1AssessedChangePct": 3.05,
    "levy2Year": 2024,
    "levy2TaxPaid": 6224,
    "levy2TaxChangePct": 2.88,
    "levy2AssessedValue": 668600,
    "levy2AssessedChangePct": 6.41,
    "levy3Year": 2023,
    "levy3TaxPaid": 6050,
    "levy3TaxChangePct": -1.07,
    "levy3AssessedValue": 628300,
    "levy3AssessedChangePct": 5.18,
    "collectedAt": "2026-06-30T10:32:00+00:00",
    "errorMessage": null
}
```

#### Output fields

| Field | Type | Description |
|-------|------|-------------|
| `queryInput` | string | The exact value you submitted — ID, URL, or address |
| `zillowPropertyId` | string | The Zillow property ID the row resolved to |
| `sale{n}Date` | string | Date of sale event n |
| `sale{n}Price` | integer | Sale amount in USD |
| `sale{n}PricePerSqft` | integer | Price per square foot |
| `sale{n}ChangePct` | number | Percent change from the previous event; negative means a drop |
| `sale{n}EventType` | string | Event label such as Sold, Listed for sale, or Price change |
| `sale{n}ListingSource` | string | MLS or listing source name |
| `sale{n}BuyerAgent` | string | Buyer agent name when shown |
| `sale{n}SellerAgent` | string | Seller agent name when shown |
| `levy{n}Year` | integer | Tax year |
| `levy{n}TaxPaid` | number | Annual tax billed in USD |
| `levy{n}TaxChangePct` | number | Year-over-year change in tax billed as a percent |
| `levy{n}AssessedValue` | integer | Assessed value in USD |
| `levy{n}AssessedChangePct` | number | Year-over-year change in assessed value as a percent |
| `collectedAt` | string | ISO 8601 time the row was captured |
| `errorMessage` | string | Present only when the lookup failed after every retry |

### Tips for best results

- **Start with a handful.** Run 5 to 10 properties first to confirm the columns line up with your pipeline before sending a big list.
- **Match the timeout to the batch.** The 360-second default suits short runs. Budget about 30 seconds per property for longer lists, and set the same value under **Timeout** in your Apify run settings.
- **Mix input types freely.** IDs, URLs, and addresses can all sit in the same run; the actor resolves each to a property ID before pulling history.
- **Skip the ID lookup when you can.** Passing a property ID or a URL with the ID in the path goes straight to history, while a bare address or slug needs one extra resolve step.
- **Filter failed rows by `errorMessage`.** When a lookup fails after all retries, the actor still writes a row with `errorMessage` filled in so you can re-run just those inputs.

### How can I use Zillow tax history data?

**How can I use the Zillow Tax History Scraper to pull property tax records in bulk?**
Paste a list of Zillow property IDs, listing URLs, or addresses, and each one comes back as a row holding up to 10 years of tax history — `levy{n}Year`, `levy{n}TaxPaid`, `levy{n}AssessedValue`, and the year-over-year change percentages. It is a quick way to assemble a tax dataset for a whole street or ZIP without copying numbers by hand.

**How can I get Zillow price history for comparative market analysis?**
Each property row carries up to 10 sale and listing events in `sale{n}Date`, `sale{n}Price`, `sale{n}PricePerSqft`, and `sale{n}EventType`. Pull the same data for a set of nearby homes, line up the price-per-square-foot, and you have the backbone of a CMA ready for a spreadsheet or BI tool.

**How can I track assessed value changes for a property tax appeal?**
Look up a single address and read the `levy{n}AssessedValue` and `levy{n}AssessedChangePct` columns across the years on record. Seeing how the assessment moved relative to the actual tax billed gives you the figures to build an appeal around.

**How can I export Zillow property history to CSV or Excel?**
Run the actor, then use Apify's export options to download the dataset as CSV, Excel, JSON, or XML. Because every property is already a flat row of numbered columns, the file opens cleanly in any spreadsheet with no reshaping needed.

### Is it legal to scrape data?

Our actors are ethical and do not extract any private user data, such as email addresses or private contact information. They only extract what the user has chosen to share publicly. We therefore believe that our actors, when used for ethical purposes by Apify users, are safe.

However, you should be aware that your results could contain personal data. Personal data is protected by the GDPR in the European Union and by other regulations around the world. You should not scrape personal data unless you have a legitimate reason to do so. If you're unsure whether your reason is legitimate, consult your lawyers.

You can also read Apify's blog post on the [legality of web scraping](https://blog.apify.com/is-web-scraping-legal/).

### Support

Questions, feature requests, or a field you'd like added? Reach out at <data.apify@proton.me> and we'll get back to you.

# Actor input Schema

## `propertyIds` (type: `array`):

Zillow ZPIDs to look up, one per line. Each ID returns its own set of rows in the dataset.

## `listingPageUrls` (type: `array`):

Zillow home detail page links, one per line. Works whether or not the property ID sits in the URL path.

## `propertyAddresses` (type: `array`):

Full street addresses, one per line. The actor resolves the matching property ID for you before pulling history.

## `timeoutSeconds` (type: `integer`):

Total wall-clock budget for the run. The 360-second default fits short lists; budget roughly 30 seconds per property for bigger ones (50 properties = 1500, 100 = 3000). Keep this in line with the Timeout value in your Apify run settings.

## Actor input object example

```json
{
  "propertyIds": [
    "29489015",
    "32345923"
  ],
  "listingPageUrls": [
    "https://www.zillow.com/homedetails/1234-Maple-Grove-Denver-CO-80205/29489015_zpid/"
  ],
  "propertyAddresses": [
    "1234 Maple Grove Dr, Denver, CO 80205"
  ],
  "timeoutSeconds": 360
}
```

# 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 = {
    "propertyIds": [
        "29489015",
        "32345923"
    ],
    "listingPageUrls": [
        "https://www.zillow.com/homedetails/1234-Maple-Grove-Denver-CO-80205/29489015_zpid/"
    ],
    "propertyAddresses": [
        "1234 Maple Grove Dr, Denver, CO 80205"
    ],
    "timeoutSeconds": 360
};

// Run the Actor and wait for it to finish
const run = await client.actor("data_api/zillow-tax-history-scraper-cheap").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 = {
    "propertyIds": [
        "29489015",
        "32345923",
    ],
    "listingPageUrls": ["https://www.zillow.com/homedetails/1234-Maple-Grove-Denver-CO-80205/29489015_zpid/"],
    "propertyAddresses": ["1234 Maple Grove Dr, Denver, CO 80205"],
    "timeoutSeconds": 360,
}

# Run the Actor and wait for it to finish
run = client.actor("data_api/zillow-tax-history-scraper-cheap").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 '{
  "propertyIds": [
    "29489015",
    "32345923"
  ],
  "listingPageUrls": [
    "https://www.zillow.com/homedetails/1234-Maple-Grove-Denver-CO-80205/29489015_zpid/"
  ],
  "propertyAddresses": [
    "1234 Maple Grove Dr, Denver, CO 80205"
  ],
  "timeoutSeconds": 360
}' |
apify call data_api/zillow-tax-history-scraper-cheap --silent --output-dataset

```

## MCP server setup

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

```

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/GIYVbHggr7w4kKxLO/builds/oFeuRltIS9oAYdpeY/openapi.json
