# Changelog of Zillow Scraper (`sleek_waveform/zillow-scraper`) Actor

- **URL**: https://apify.com/sleek_waveform/zillow-scraper/changelog.md
- **Full Actor documentation**: https://apify.com/sleek_waveform/zillow-scraper.md

## Changelog

### 1.0.13 — 2026-08-01

- **Unrecognised locations no longer fail the run.** When Zillow resolves a
  location to the wrong region (typo or nonsense input), every listing is still
  discarded and nothing is charged, but the run now ends **SUCCEEDED** with the
  explanatory status message instead of FAILED. A bad location is the customer's
  input problem, not an actor failure — Apify QA probes with nonsense locations
  (e.g. "Zzzqqq Nowhere, XX") and flags actors whose runs go FAILED, and the
  failure webhook paged on every customer typo. Genuine retrieval failures
  (blocks after retries) still fail the run, and now take precedence over
  wrong-region discards. Decision logic extracted to `run_outcome.py` with
  regression tests (`tests/test_run_outcome.py`).

### 1.0.10 — 2026-07-31

- **Autofix monitoring wired in.** The actor now registers its failure webhook
  (`ACTOR.RUN.FAILED` / `TIMED_OUT` / `ABORTED`) with the monitoring receiver and
  self-reports RUN_STARTED / RUN_FINISHED telemetry, like the rest of the portfolio.
- **Fixed a silent, portfolio-wide monitoring bug.** `WebhookEventType` stopped
  being an Enum in apify SDK 4.x and became a `typing.Literal` union, so
  `WebhookEventType.ACTOR_RUN_FAILED` raised `AttributeError`. Registration is
  best-effort and swallows errors, so this failed invisibly — **no Python actor
  had been registering failure webhooks, and autofix never fired for them.**
  Event types are now passed as plain strings, which work on both old and new SDKs.
  Fixed in the canonical `_shared/apify_monitor.py` and synced to all Python actors.
- Verified live: webhook registers, and an aborted run is recorded by the receiver.
- Pricing switched from flat $0.002 to plan-tiered ($0.0020 → $0.0004).

### 1.0.5 — 2026-07-31

Fixes found by running the actor on the Apify platform rather than locally.

- **Wrong-region results (critical).** Filters were passed as a `searchQueryState`
  query parameter. Without map bounds, Zillow ignores the location in the URL
  path and resolves the search against the **proxy exit IP's** geography — a run
  for "Austin, TX" stored Lewistown, PA listings, and "Denver/Tampa" returned
  Chicago, IL. Filters are now URL **path segments**, which preserve the region.
  Triggered by `sortBy` having a schema default, so *every* run took the filtered
  path.
- **Region guard.** Every page is checked against the requested state/ZIP. A
  search that comes back for the wrong area is discarded, and the run fails with
  an explanatory message instead of storing and billing foreign listings.
- **Filters Zillow silently ignores.** `..._sqft`, `manufactured_type` and
  comma-joined multi-type segments are accepted and then not applied. All filters
  are now re-checked locally on every row before it is stored or billed.
- **Multi-type efficiency.** Selecting several home types yielded 16 of 60 rows at
  10x the cost per row. Multiple types now fan out into one search per type,
  restoring server-side narrowing (60/60 rows, $0.000029/row).

### 1.0 — 2026-07-31

First release.

#### Features

- Scrape Zillow search results by city, ZIP code, or raw Zillow search URL.
- Three listing statuses: **for sale**, **for rent**, **recently sold**.
- Filters: min/max price, min beds, min baths, min/max living area, home type,
  and sort order — passed through Zillow's own `searchQueryState`.
- 40+ normalised fields per listing, including `zestimate`, `rentZestimate`,
  and `taxAssessedValue`.
- Rental buildings return per-unit floor plans (`units`, `minRent`, `maxRent`,
  `availableUnits`).
- Pay-per-result billing with an empty-search floor: a search that matches
  nothing is never charged.

#### Implementation notes

- **Access**: Chrome TLS fingerprint impersonation (`curl_cffi`) over Apify
  RESIDENTIAL proxies. Verified that neither alone is sufficient — a plain
  request and a TLS-impersonating request from a datacenter IP are both
  refused; the combination succeeds.
- **Sticky sessions**: a proxy session is reused until it is blocked, then
  rotated. Measured 12/12 successful pages on a warmed session versus 10/12
  when every request took a fresh IP.
- **No browser**: listings are read from the page's embedded JSON, so the actor
  is HTTP-only. This keeps compute — and therefore price — low.
- **Detail pages are not supported.** Zillow refuses `/homedetails/` pages even
  from sessions whose search pages return 200 (verified 0/6). Detail URLs are
  rejected at input time rather than failing mid-run. The valuation fields that
  motivate detail scraping are present in search results regardless.

#### Correctness safeguards

- HTTP 200 is never treated as proof of data; blocks are detected structurally
  and retried on a fresh session.
- Billing is idempotent per listing, so retries and overlapping pages cannot
  double-charge (see `tests/test_billing.py`).
- Pagination is sized from Zillow's reported match count, not from a short page
  — the same URL can return 41, 7, or 15 rows depending on the exit IP.
