Populates the flat broker summary columns from the collected broker records.
- 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.
Repairs map-area searches (placeIds[]), which stopped resolving after Crexi replaced its search client.
- 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.
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.
Major rework focused on reliability, data quality and Store discoverability.
- 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.
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.
- 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.
- 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.