# Changelog of Crexi Scraper \[💰$3.5/1K] | Commercial CRE | Broker Leads (`ahmed_jasarevic/crexi-property-broker-data-scraper-pro`) Actor

- **URL**: https://apify.com/ahmed_jasarevic/crexi-property-broker-data-scraper-pro/changelog.md
- **Full Actor documentation**: https://apify.com/ahmed_jasarevic/crexi-property-broker-data-scraper-pro.md

## Changelog

### 2.1.3

Populates the flat broker summary columns from the collected broker records.

#### Fixed

- The search payload carries no broker name, so `brokerName` stayed null even when the
  `includeBrokers` add-on had collected full broker records. A populated `brokers[]` list next
  to an empty summary column reads as "the brokers were not saved", so both output paths now
  fill `brokerName` and `brokerageName` from the first collected broker.

### 2.1

Repairs map-area searches (`placeIds[]`), which stopped resolving after Crexi replaced its search client.

#### Fixed

- Crexi's web client no longer posts to `api.crexi.com/assets/search`; it redirects
  `/properties?placeIds[]=…` to `/search` and calls `api.crexi.com/universal-search/v2/search`
  instead. The browser lookup was still waiting for the retired request, timed out three times on
  every attempt, and failed each run that pasted a map URL. Map areas are now resolved without
  Crexi in the loop at all: `placeIds[]` are Google place ids (Crexi's map is Google Maps), and
  Google's own place page names the place — `Anchorage, AK`, `Dallas County, TX`,
  `Anchorage, AK 99501` — over plain HTTP, which clears Cloudflare where the headless browser is
  turned away. The browser, with the maps library the Crexi page ships, stays as the fallback
  for an id Google's page would not name.
- The search API validates `locations[].PlaceId` and then ignores it, and polygon points are not
  read at all (verified by walking the validation errors and comparing result counts), so map
  polygons could no longer filter anything. Only the area filters the endpoint actually applies
  are sent now: `states`, `cities`, `counties` and `zips`, each verified against the live API.
- A URL mixing place kinds — a city place id and a state one, say — is rejected with an actionable
  message. Crexi combines different area filters with AND, so such a URL used to be intersected
  into an empty result set.
- When crexi.com answers with a bot challenge instead of the search page, the failure now says so
  instead of reporting a generic "captured no map filter" timeout. A resolved area also carries
  its state, so a city that repeats in another state cannot leak into the results.

#### Changed

- `resolveLocations` is now `resolveLocationFilters` and returns
  `{states, cities, counties, zips}` instead of polygon objects.
- `buildSearchPayload` takes `locationFilters` instead of `locations` and merges the resolved
  state into the `states` filter rather than forwarding an ignored `locations` array.

### 2.0

Major rework focused on reliability, data quality and Store discoverability.

#### Added

- Structured filter inputs: property types, states, deal type (sale/lease), keyword, asking price
  range, cap-rate range, price per square foot, building size, sort order and sort direction.
  Previously filters could only be supplied by hand-crafting a Crexi search URL.
- Three optional enrichment add-ons, each billed as its own pay-per-event: listing details,
  listing brokers and photo galleries. Crexi's per-listing API routes are now discovered from a
  single real browser page load instead of being hard-coded.
- Direct support for single-listing URLs. A pasted `crexi.com/properties/<id>/...` page is now
  fetched from the single-asset endpoint instead of being sent through search, which previously
  ignored it. Property pages and search pages can be mixed in one run.
- Full dataset schema with documented fields (`title`, `description`, `example` per field) and
  four focused output views: Overview, Location, Financials and Brokers.
- Pay-per-event catalog in `.actor/pay_per_event.json` covering the three add-ons. Collected
  listings are billed by Apify's own synthetic `apify-default-dataset-item` event.
- Run progress reporting through the Actor status message, plus a summary log line with the
  number of listings collected and API requests made.
- `CHANGELOG.md` and memory limits in `actor.json`.

#### Changed

- `axios` is now an explicit dependency. It was imported by `main.js` but only resolved by
  accident as a transitive dependency.
- The `proxyConfiguration` input is now actually applied to every request. It was previously
  advertised in the UI and silently ignored.
- Map filters (`placeIds[]`) are resolved with a short, bounded browser lookup that now uses
  `page.waitForRequest` and reuses a single browser. Previously it launched a new browser three
  times, waited up to 35 seconds per attempt, and silently fell back to an unfiltered nationwide
  scrape when it failed. The run now fails with an actionable message instead.
- Request failures retry with exponential backoff before pagination gives up.
- The code is split into `src/` modules: `config`, `payload`, `http`, `browser`, `normalize`,
  `scrape`.
- Output records are flattened and documented. Previously the raw search-API object was pushed
  unchanged, while the README documented fields that were never produced.
- `maxResults` renamed to `maxItems` (Apify convention). The old field still works.
- The `states` input now actually filters results. It was displayed in the UI and logged to the
  run summary but never reached the search, so a "Texas only" run returned the whole country.
  Crexi's search API honours state codes server-side, and every record is re-checked locally as
  a guard so an out-of-scope listing can never reach the dataset.
- Asset-class and state filters are also read from pasted search URLs (`types[]`, `states[]`).
  Crexi uses `types[]`, which was not previously recognised, so pasted type filters were dropped.
- De-duplication is shared across every start URL of a run. Previously each URL had its own set,
  so a listing reachable from two overlapping searches was pushed twice.
- Browser proxy credentials are now passed correctly. The proxy server string was being stripped
  of everything before the `@`, which discarded the username and password.

#### Removed

- The free-user result cap based on `APIFY_USER_IS_PAYING`. Free usage is now governed by
  Apify's own pricing configuration instead of an environment variable.
- Redundant `npx playwright install chromium` build step. The base image already ships Chromium.
- Unused `cheerio` and `crawlee` dependencies.
- The "SEO Keywords" keyword list from the README and placeholder links to non-existent Actors.

#### Fixed

- Search pagination sent `skip`, which Crexi ignores completely. Every page after the first
  returned the same 50 listings, so a run asking for 100 items collected 49 and reported success.
  The endpoint paginates on `offset`.
- A page consisting only of already-seen listings no longer ends the run early. Pagination now
  continues until Crexi returns an empty page.
- Crexi's reported `totalCount` is far larger than the number of listings it will serve (it stops
  around 1,460 even when claiming thousands). The run no longer treats that number as proof of
  exhaustion and no longer suggests raising `maxItems` when more listings are unreachable.
- Property-type and keyword filters were sent as `propertyTypes` and `query`, which Crexi accepts
  and then ignores, so a filtered search returned unrelated asset classes. They are now sent as
  `types` and `term`.
- Removed the per-result `charge()` call. Apify already bills the synthetic
  `apify-default-dataset-item` event for every row pushed to the dataset, so charging `result` as
  well billed users twice and logged `unknown event` warnings on every listing.
- Building size and price-per-square-foot filters were forwarded to Crexi, which ignores them.
  They are now applied per record before the row is saved.
- Playwright could not launch in the cloud because the base image ships a different Chromium
  revision than the installed Playwright expects. The build now installs a matching browser.
- Add-on routes are no longer taken from browser sniffing alone, which missed every request and
  silently disabled the add-ons. Verified Crexi routes are the default and discovery only
  overrides them when it finds something concrete.
- Broker fields are nullable, so a listing with no published phone or email no longer fails
  dataset schema validation.
- A single API error no longer ends the run with a silent partial result; it is retried, and
  only stops pagination once some data has been collected.
- Listings appearing on several pages are de-duplicated by Crexi ID.
- Multiple start URLs are all scraped, not just the first one.
- The daily Store health test no longer defaults to a 500-listing run. `maxItems` now prefills
  at 10 so the automated test is fast and cheap.
- `maxRequestRetries` now counts retries after the first attempt, matching its label. It was
  used as a total attempt count, so the default of 3 allowed only 2 retries.
- Property URL parsing is restricted to crexi.com, so an unrelated `/properties/<id>` URL is no
  longer mistaken for a Crexi listing.
- A listing with no state is skipped rather than assumed to match when states are selected.
