# Changelog of Zillow Property Detail Scraper (`studioxl/zillow-detail-scraper`) Actor

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

## Changelog

### 0.1.26 (2026-09-24)

- **An address that is not on Zillow no longer fails the run.** The address field
  has always said unmatched addresses are skipped rather than failing the run. That
  held only while at least one other address matched. A run given a single address
  Zillow does not know, which is how an automation sending one property per run
  uses this Actor, failed. It now succeeds, delivers nothing, charges nothing and
  says which address was not found.
- **Addresses ending in ", USA" or a ZIP+4 now resolve.** That is the format Google
  Maps, most CRMs and spreadsheet exports produce, and Zillow's lookup handles it
  badly. Of 24 real addresses written that way, 4 resolved to the right property,
  4 to a different house and 16 to nothing. With the suffix and ZIP extension
  stripped before lookup, all 24 resolve correctly.
- **The lookup can no longer return a different house.** A match must now carry
  the same house number, and the same ZIP when you give one. "1847 S Clarkson St,
  Denver, CO 80210, USA" had been resolving to 1847 N Clarkson St in 80218 and
  charging for it. A near miss is now reported as not found, at no charge.
- **The address lookup retries.** It was a single request with no retry, so one
  refused connection could decide a one-address run. It now tries three times, the
  second and third through the proxy on a fresh IP.
- **A Zillow URL typed into the address field is treated as a URL,** and field
  names that API and AI-agent callers commonly guess (`url`, `urls`,
  `propertyUrls`, `address`, `zpids`) are read rather than ignored. When a run
  still has nothing to scrape, the message names the fields it received and did
  not recognise.
- **An empty search dataset is an empty result.** A chained run whose upstream
  search found nothing now succeeds with nothing to scrape instead of failing.

### 0.1.24 (2026-09-17)

- **Uploaded and linked URL lists now work.** The URL field lets you upload a text
  file or link one instead of typing URLs, and those arrive in a different shape.
  Only typed URLs were being read, so a run given a file of properties failed with
  "Nothing to scrape". Zillow URLs are now read out of the file, with or without
  the https:// in front.
- **Removed listings no longer fail the run.** A property that has left Zillow
  returns HTTP 404. When every URL in a run had been removed, the run failed and
  blamed the proxy configuration. It now succeeds, delivers nothing, charges
  nothing, and says how many listings were removed.
- **Address search URLs are accepted.** Typing an address into Zillow's search box
  leaves a /homes/<address>\_rb/ URL in the address bar. That page is disallowed by
  Zillow's robots.txt and was rejected. The address in it is now looked up the same
  way a typed address is.
- **Clearer message when no property page is supplied.** Pasting a search results
  page now says to run the Zillow Search Scraper first and pass its dataset here.
- **A Zillow ID written as 2146999031\_zpid is recognised** in the address field.

### 0.1.19 (2026-08-30)

- **Apartment building and `/b/` pages worked again.** Both URL shapes had started
  failing outright: five residential retries each, then a give-up, with
  `'str' object has no attribute 'get'`. Cause was a name collision inside the shared
  parser, where a helper added for the *search* Actor's rental-building rows shadowed
  the same-named helper the detail parser depends on, so `normalise_building` was
  handed a summary dict where it expected a list of units. The search Actor was never
  affected. Verified after the fix: an apartment building returns 24 units, rents
  $810 to $1,200, 1 to 2 beds, the leasing phone and 24 photos.
- **Five fields removed after measuring 745 properties.** `brokerPhone`,
  `brokerOfficeName`, `agentLicenseNumber`, `stories` and `tourViewCount` were empty
  on every single row across Austin, Chicago, Denver, Greenville, Miami and Phoenix.
  Zillow carries the keys but does not populate them on listing pages. Three of these were
  added in 0.1.15 on the strength of where they sit in Zillow's payload rather than
  on a measurement, which was the mistake. A permanently-empty column is worse than
  no column, so they are gone and the honest field count is 60, not 65.
- **Published coverage for every remaining field**, measured on those same 745
  properties. Some of it is unflattering and is printed anyway: Zestimate arrives on
  19% of properties, rent Zestimate on 21%, and county on 4%. On the first 300
  properties Zestimate looked like 35%; the wider sample halved it, which is exactly
  why the number is published rather than estimated.
- **Builder communities re-verified.** Three of four live community URLs returned
  builder name, builder phone, price range, spec-home and plan counts, and 11 to 21
  photos. The community URL that another Zillow Actor ships as its prefilled example
  returns HTTP 404.
- **Default memory raised to 1 GB**, with 4 GB available. A 445-property run peaked
  at 289 MB, and memory grows with the deduplication state a chained run builds up.
  Compute is 1% of what a property costs against 99% for residential proxy, so the
  headroom is nearly free.
- **Output schema added**, which the Actor needs before it can be published.
- **Honest blocking count.** The end-of-run line used to read "0 blocked" even on runs
  that absorbed 100 HTTP 403 responses, because the counter only ever meant "requests we
  gave up on". It now reads "0 lost to blocking after retries", which is what the number
  has always measured. The input schema's block-rate figure was also wrong: it claimed
  3.2% of first attempts, and a 445-property run measured about 22%. All 445 were still
  delivered, because a retry is never a charged row.
- **Store copy rewritten.** README, Actor description and input help text.

### 0.1.15

- **Three new agent fields:** `agentLicenseNumber`, `brokerPhone` and
  `brokerOfficeName`. The licence number is published by most MLS feeds but sits in a
  different part of Zillow's payload from the other agent details, so it was being
  missed. It is the field that lets you match an agent to a state registry.

### 0.1.14

- **Apartment buildings and builder communities now actually work.** Both URL shapes
  were advertised but neither could be parsed: Zillow serves three different payloads
  and only single-home pages carry the one the parser knew about. A building URL used
  to burn five residential retries and return nothing. Buildings now return unit
  count, rent/bed/size ranges and leasing contact; communities return spec homes,
  buildable plans, and the builder's name and phone. Every row carries a `pageType`
  column so a mixed export still reads as one table.
- **New: expand buildings into individual units.** One row per rentable unit, or per
  home in a community, each with its own Zillow ID, beds, baths, size and price. The
  units are already on the page being fetched, so this costs no extra request, but
  each one is a charged row, so it is off by default.
- **`/b/…/lat,long_ll/` URLs are now skipped.** Zillow's robots.txt disallows them
  explicitly (`Disallow: /b/*/*_ll/`). The `/b/name-city-st-id/` form is permitted and
  works. Worth knowing if you are migrating: another Zillow Actor prefills the
  disallowed shape as an example.
- **Generated map images are no longer counted as photos.** Off-market homes carry a
  Google satellite tile or Street View frame where listing photos would be; those URLs
  are signed and expire. They are now excluded, so `photos` only ever contains real
  listing photos and `photoCount` always matches the list beside it. Affected rows go
  from `photoCount: 1` to `photoCount: 0`.
- **The run now stops at your maximum charge.** If you cap what a run may cost, the
  crawl stops the moment that cap is reached instead of continuing to fetch pages
  that can no longer be billed. Previously the charge result was discarded, so a
  capped run kept crawling to the end.
- **Fixed monitoring for buildings and communities.** Their change fingerprint was
  computed from property-only fields, all of which are absent, so every building
  hashed to the same constant and would have been reported unchanged forever. Price
  range, unit count and inventory count are now part of the fingerprint. One-off
  effect: the first run after this update reports everything in an existing memory
  store as changed.

### 0.1.10

- **Roughly twice as fast, and slightly cheaper.** The old 120 requests/minute cap,
  not concurrency, was what decided how long a run took. The crawler spent most of
  its time idle waiting for the limiter. New defaults are 10 parallel requests at 300
  per minute: measured on 1,000 properties, 4m09s instead of 8m36s and $0.613 instead
  of $0.638, with all 1,000 delivered.
- **Default memory 2048 MB → 512 MB.** Peak usage measured at 211 MB. This cuts the
  compute portion of a run about fourfold and leaves far more room to run several
  scrapes in parallel on your plan.

#### Known cosmetic issue

Some runs print an `aclose(): asynchronous generator is already running` traceback
*after* finishing. It is harmless, the run exits 0 and every record is delivered; the
line above it always reads `SystemExit: 0`. It comes from a connection being torn down
inside the Apify SDK during interpreter shutdown, not from this Actor's own code, and
we could not eliminate it from Actor code. If you are scripting against this Actor,
trust the exit code and the item count, not the presence of a traceback.

### 0.1.9

- **Fixed chaining to a search dataset.** The Actor runs under limited permissions, so
  its run token had no access to a dataset it did not create, so passing a
  `searchResultsDatasetId` failed with an insufficient-permissions error. The field is
  now a dataset picker that declares `READ`, which makes the platform grant access to
  the dataset you choose. Also nicer to use: pick from a list instead of pasting an ID.

### 0.1.8

- **"Maximum properties" now actually caps spend.** It previously stopped writing
  results but let the crawl keep fetching every remaining URL, so a 100-property cap
  against a large chained dataset still paid to download the whole thing.
- **Duplicate input URLs are dropped before fetching**, not after. The same property
  supplied twice used to cost two full page fetches.

### 0.1.7

- **Removed `agentEmail`.** Zillow suppresses agent email on listing pages, so the
  column was empty on every property. Agent email comes from agent profile pages and
  will be covered by a dedicated agent Actor. Output is now 61 fields.
- Rewrote the README: scannable tables for all 61 output fields, with measured
  coverage on the agent fields, and a real output example instead of an invented one.
- Documented that Zestimates are US-only, and that `zillow.com` carries Canadian
  listings where they are unavailable.
- Corrected the proxy guidance: residential US is required, not a fallback.

### 0.1.3

- Confirmed at scale: 1,000 properties in 8m36s with no blocking.
- Accepts street addresses and bare Zillow IDs alongside URLs, resolved through the
  Zillow autocomplete endpoint. Unmatched addresses are reported and skipped instead
  of failing the run.
- Chaining: `searchResultsDatasetId` scrapes every property found by a search run.

### 0.1.2

- Photo width selector, 192px to 1536px. URLs only, never image bytes, so photo
  count does not affect run cost.
- Re-listed properties (same home, new Zillow ID) are matched on coordinates, price
  and size so you are never charged twice for the same house.

### 0.1.1

- Incremental mode: return only properties that are new or whose price, Zestimate,
  rent estimate, status or description changed. Unchanged properties are never
  charged. View and favourite counts are excluded from change detection because they
  move on their own and would mark every property changed on every run.
- An empty run that fetched nothing is reported as a fetch failure rather than as
  "no results". Those look identical from outside and only one is your problem.

### 0.1.0

- First release. Full property record from `/homedetails/`, `/apartments/` and
  `/community/` URLs, with robots.txt honoured via real longest-match rule
  resolution.
