# Zillow API — full reference for LLM context A REST API for U.S. real estate data. Built to be consumed by AI agents and SaaS products. White-label: customers never see vendor names in responses, error messages, or job records. Base URL: `https://api.zillapi.com` Auth header: `Authorization: Bearer zk_…` OpenAPI 3.1 spec: `https://zillapi.com/openapi.json` ================================================================ # Authentication API keys are issued through the dashboard. Format: `zk_<43-char-base64url>`. The plaintext key is shown ONCE at creation; only the SHA-256 hash is stored server-side. To rotate, create a new key, switch traffic, then revoke the old one. Revocation takes effect within seconds. Errors: - 401 `missing_api_key` — no Authorization header - 401 `invalid_api_key` — bad format / unknown / revoked - 403 `account_suspended` — plan inactive - 429 `quota_exceeded` — monthly result quota hit - 429 `rate_limited` — per-minute rate limit hit ================================================================ # Errors Universal envelope: ``` { "error": { "code": "...", "message": "...", "request_id": "..." } } ``` `error.code` is stable; `error.message` may evolve. Match on code, never prose. Always include `request_id` in support tickets. HTTP code mapping: - 200 success · 201 created · 202 accepted (async) · 204 no content - 400 client validation · 401 auth · 403 suspended · 404 not found - 409 wrong job state · 429 rate/quota · 502 upstream · 504 upstream timeout Common codes: `missing_api_key`, `invalid_api_key`, `quota_exceeded`, `rate_limited`, `invalid_url`, `invalid_address`, `invalid_zpid`, `missing_input`, `invalid_status`, `invalid_extract_units`, `not_found`, `job_not_ready`, `job_not_found`, `upstream_timeout`, `upstream_error`, `invalid_json`. ================================================================ # Rate limits & quotas Two independent limits per API key: 1. Per-minute rate (sliding window). 2. Monthly result-unit quota. | Plan | Rate (req/min) | Monthly quota | |---|---:|---:| | Free | 20 | 100 | | Starter | 60 | 5,000 | | Pro | 300 | 50,000 | | Scale | 1,500 | 500,000 | Cache hits on `/v1/properties/{zpid}` and sub-resources cost 0 units (fresh = ≤ 24h old). ================================================================ # Output formats & projection `?format=json` (default), `?format=csv`, `?format=ndjson` — also driven by `Accept` header. - NDJSON: one JSON object per line, no envelope. - CSV: nested fields flattened with dot-notation keys; arrays JSON-stringified. Field projection on detail endpoints: `?fields=zpid,address.streetAddress,price,priceHistory[0].price`. Supports dotted paths and `[n]` array indexing. ================================================================ # Async jobs Endpoints that return `202 { data: { job_id, status } }`: - `POST /v1/properties/batch` (always async) - `POST /v1/search` (when `maxItems > 50` or `extractionMethod=PAGINATION_WITH_ZOOM_IN` or `async: true`) - `POST /v1/search/with-details` (always async, two-stage chained) - `GET /v1/buildings/by-url?sync=false` Track via: 1. Polling `GET /v1/jobs/{id}` (status terminal: succeeded, failed, timed_out, aborted). 2. Webhook (recommended for production). Fetch results: `GET /v1/jobs/{id}/results?limit=&offset=&format=`. Caps: limit max 1000, offset unbounded. ================================================================ # Webhooks (outbound) Customer registers a URL via `POST /v1/webhooks`. We POST signed events when a job becomes terminal. Headers we send: - `X-Zillow-Signature: t=,v1=` — HMAC-SHA256 over `.` with the webhook secret. - `X-Zillow-Event: job.{succeeded|failed|timed_out|aborted}` - `Content-Type: application/json` - `User-Agent: zillow-api-platform/1.0` Payload: ``` { "event": "job.succeeded", "delivered_at": "...", "data": { "job": { "id": "...", "type": "...", "status": "...", "result_count": 213, "..." } } } ``` Verification: regex-match the header, check timestamp skew < 5 min, recompute HMAC, constant-time compare. See `/recipes/verify-webhook/` for code in Node, Python, Go. Delivery: 8s timeout, up to 3 attempts (initial + 2 retries) with quadratic backoff. Each attempt logged in `/v1/webhooks/{id}/deliveries`. ================================================================ # API REFERENCE ## Properties ### GET /v1/properties/by-url Query: `url` (required), `status` (FOR_SALE|RECENTLY_SOLD|FOR_RENT, default FOR_SALE), `extract_units` (disabled|all|for_sale|recently_sold|for_rent|off_market, default disabled), `fields`. Response: `{ data: , request_id }`. If extract_units != disabled and the URL is multi-unit, `data` is an array. ### GET /v1/properties/by-address Query: `address` (required), `status`, `fields`. Response: `{ data: , request_id }`. ### GET /v1/properties/{zpid} Cache-first (24h TTL). Response: `{ data, cached, fetched_at, request_id }`. ### Sub-resources (cache-served when fresh) - GET /v1/properties/{zpid}/photos → photos array + counts + has_3d/has_video - GET /v1/properties/{zpid}/price-history → priceHistory array - GET /v1/properties/{zpid}/tax-history → taxHistory array - GET /v1/properties/{zpid}/schools → schools array - GET /v1/properties/{zpid}/nearby → nearbyHomes array - GET /v1/properties/{zpid}/agent → agent + broker contact - GET /v1/properties/{zpid}/zestimate → zestimate, rent_zestimate, tax_assessed_value, last_sold_price, currency - GET /v1/properties/{zpid}/open-houses → schedule + tour_eligibility - GET /v1/properties/{zpid}/facts → resoFacts (full MLS attribute set) ### POST /v1/properties/batch Body: `{ urls?: string[], addresses?: string[], propertyStatus?, extractBuildingUnits?, maxItems? }`. Up to 500 entries. Response: 202 `{ data: { job_id, status } }`. ## Buildings ### GET /v1/buildings/by-url Query: `url` (required, must be /b/, /apartments/, /community/), `include_units` (default all), `sync` (default true). Sync response: `{ data: { units: [...] }, meta: { count, include_units } }`. Async response: 202 `{ data: { job_id, status } }`. ## Listings (status-preset sugar over /v1/search) POST /v1/listings/for-sale POST /v1/listings/for-rent POST /v1/listings/sold Body: same as /v1/search. Response: same as /v1/search (sync 200 or async 202). ### GET /v1/listings (REST wrapper) Query: `status` (default for_sale), `bbox` (w,s,e,n) OR `location`, `price_min/max`, `beds_min/max`, `baths_min/max`, `sqft_min/max`, `year_built_min/max`, `home_types` (comma-separated), `days_on_zillow`, `max_items` (≤50), `format`. Sync only. ## Search ### POST /v1/search Body option A (recommended): ``` { "filters": { "status": "for_sale|for_rent|sold", "bbox": { "west": -..., "south": ..., "east": ..., "north": ... }, "location": "City, ST", "price": { "min": 0, "max": 0 }, "beds": { "min": 0, "max": 0 }, "baths": { "min": 0, "max": 0 }, "sqft": { "min": 0, "max": 0 }, "yearBuilt": { "min": 0, "max": 0 }, "homeTypes": ["house","condo","townhouse","multi_family","manufactured","lot","apartment"], "daysOnZillow": "1|7|14|30|90|6m|12m|24m|36m", "hasPool": false, "hasGarage": false, "hasAirConditioning": false, "hasBasement": false, "isWaterfront": false }, "extractionMethod": "PAGINATION|MAP_MARKERS|PAGINATION_WITH_ZOOM_IN", "maxItems": 50, "async": false } ``` Body option B (advanced): `{ "searchUrls": [{ "url": "https://www.zillow.com/...?searchQueryState=..." }] }`. Response: sync 200 or async 202. ### POST /v1/search/with-details Same body as /v1/search plus `propertyStatus` and `extractBuildingUnits`. Always async. Returns `{ data: { job_id, status, stage: "search" } }`. Final results are detail rows, not search rows. ## Jobs ### GET /v1/jobs Query: `status`, `type`, `since`, `limit` (≤500), `offset`. Returns list of job rows with id, type, status, result_count, error, chain_stage, timestamps. ### GET /v1/jobs/{id} Single job row. ### GET /v1/jobs/{id}/results Query: `limit` (≤1000), `offset`, `format` (json|csv|ndjson). 409 if status != succeeded. ## Webhooks ### POST /v1/webhooks Body: `{ url, events?, description? }`. Default events: all four. Response 201 includes `secret` field — plaintext, shown ONCE. ### GET /v1/webhooks Returns array of `{ id, url, events, active, description, created_at, revoked_at }`. Never returns secret. ### DELETE /v1/webhooks/{id} Soft-revoke. 204. ### GET /v1/webhooks/{id}/deliveries Per-attempt log: `{ id, job_id, event, attempt, status_code, delivered, attempted_at, response_preview }`. Useful for debugging delivery failures. ## Account ### GET /v1/me Returns account, plan summary, and `usage: { this_period, remaining }`. ### GET /v1/usage Query: `since`, `limit` (≤1000). Returns recent `{ id, endpoint, actor, units, status_code, apify_run_id (always null in public response), created_at }` rows. ================================================================ # Property object — key fields Top-level: `zpid`, `address`, `bedrooms`, `bathrooms`, `price`, `homeType` (SINGLE_FAMILY|CONDO|TOWNHOUSE|MULTI_FAMILY|APARTMENT|MANUFACTURED|LOT), `homeStatus` (FOR_SALE|RECENTLY_SOLD|FOR_RENT|...), `latitude`, `longitude`, `livingArea`, `lotSize`, `yearBuilt`, `zestimate`, `rentZestimate`. Nested: `priceHistory[]`, `taxHistory[]`, `schools[]`, `nearbyHomes[]`, `responsivePhotos[]` (with width/height), `openHouseSchedule[]`, `resoFacts` (large MLS attribute object), `tourEligibility`. Agent/broker: `agentName`, `agentEmail`, `agentPhoneNumber`, `agentLicenseNumber`, `brokerName`, `brokerPhoneNumber`, `attributionInfo`. Search results have a different shape: `zpid`, `addressStreet/City/State/Zipcode`, `price`/`unformattedPrice`, `beds`, `baths`, `area`, `latLong: { latitude, longitude }`, `statusType`, plus `hdpData.homeInfo` containing `homeStatus`, `daysOnZillow`, `listing_sub_type`, etc. ================================================================ # Agent etiquette - Cache responses for 24h on your side too. - One in-flight request per key (concurrency = 1) — scale by raising plan, not parallelism. - Back off on 429 with exponential jitter, minimum 2s between retries. - Identify with a clear `User-Agent` like `MyAgent/1.2 (+https://yourdomain.example)`. - Use sub-resource endpoints when you only need part of the detail blob — the full record is 300+ fields. ================================================================ # White-label guarantee The platform is fully white-labeled. Customer-facing responses, errors, and job records strip vendor names (e.g. provider run ids, dataset ids). The only id customers see is our `request_id`. Field names in upstream payloads (e.g. `hdpData`, `zpid`) come from Zillow itself, not from any wrapping provider.