# Commercial Real Estate Cap Rate Scraper (`arezdev/commercial-real-estate-cap-rate-scraper`) Actor

Search Crexi and extract accessible commercial property listings with asking prices, published cap rates, NOI, area and broker contacts. Filter by state, property type, price or cap rate and export structured data for investment research.

- **URL**: https://apify.com/arezdev/commercial-real-estate-cap-rate-scraper.md
- **Developed by:** [Arezdev](https://apify.com/arezdev) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $4.00 / 1,000 commercial property extracteds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

**Find commercial properties, compare published cap rates, and export acquisition-ready financials and broker contacts in one run.**

Search Crexi by US state and property type, or supply public commercial listing URLs. Get flat JSON, CSV, Excel, or an API dataset for acquisitions, investment screening, lending, and market research. The Actor uses direct HTTP requests and streams responses without running a browser.

Best for commercial acquisition screening and broker research. Start with the small Crexi sample or a public listing URL. Published values and calculated estimates are distinguished; gated financials and access to every marketplace are not guaranteed.

### Ready-to-run example

The Input form includes public example values. A fresh `apify run` uses the input schema defaults. To explicitly select the small sample, including when input was saved by an earlier local run, use:

```sh
apify run --input-file .actor/input.example.json
```

You can also paste [.actor/input.example.json](.actor/input.example.json) into the JSON input editor in Apify Console. Replace the example sources with your own targets and increase the limits when you are ready.

### How to use

1. Select states and property types, or paste direct listing URLs.
2. Optionally set a minimum cap rate or maximum asking price.
3. Set the maximum number of records and click **Start**.
4. Open the dataset to review results or export JSON, CSV, or Excel.

The Console prefill searches Texas industrial listings and saves up to three rows. The input defaults are Texas, Industrial and Retail, and 30 rows. Free Apify accounts can save at most 25 rows per run. These are live listings; availability and response times depend on the source.

Search example:

```json
{
  "states": ["TX"],
  "propertyTypes": ["Industrial", "Retail"],
  "minCapRate": 6,
  "maxAskingPrice": 5000000,
  "maxItems": 30,
  "timeoutSecs": 15
}
```

Direct URL example:

```json
{
  "directListingUrls": [
    "https://www.crexi.com/properties/2658948/texas-932-s-cesar-chavez-rd"
  ],
  "maxItems": 3
}
```

Providing `directListingUrls` selects direct mode. State and property-type search settings then do not restrict those URLs; financial filters still apply. Each direct URL must identify one listing. Arbitrary public commercial listing pages, including broker sites and LoopNet pages, are supported when they expose server-rendered listing information, Schema.org data, OpenGraph values, or attribute tables. Sources requiring JavaScript rendering, authentication, or protected financial data may return an `ERROR` row or unavailable fields.

### What data can you extract?

| Fields | Meaning |
| --- | --- |
| `platform`, `assetId`, `status` | Source, source asset identifier, and `SUCCESS` or `ERROR` |
| `propertyName`, `propertyType` | Listing name and published property categories |
| `askingPrice`, `capRatePercent` | Asking price in USD and published cap rate in percentage points |
| `estimatedAnnualNoi` | Published annual NOI when available, otherwise an estimate from price and cap rate |
| `squareFootage`, `pricePerSqFt` | Published building area and published or calculated price per square foot |
| `lotSizeAcres`, `yearBuilt` | Published acreage and construction year |
| `occupancyPercent`, `tenancyType` | Occupancy percentage and published tenancy |
| `streetAddress`, `city`, `state`, `zipCode` | Property address; ZIP codes remain strings |
| `brokerName`, `brokerCompany`, `brokerPhone` | First public broker contact, when available |
| `listingUrl`, `scrapedAt`, `errorMessage` | Listing link, UTC collection timestamp, and safe failure information |

Every row contains these exact fields. Unavailable, masked, ambiguous, or invalid source values remain `null`. Contacts and property details may be incomplete because the source limits public access. Values reflect what the source publishes; the Actor cannot verify the accuracy of listing information.

The Actor calculates `estimatedAnnualNoi = Math.round(askingPrice * (capRatePercent / 100))` only when no published NOI is available. It calculates price per square foot only when the source does not publish it and a positive building area is available. The calculated value is rounded to two decimals and stored in `pricePerSqFt`, in the flat output schema. It does not derive a cap rate from NOI or infer masked financial values. Zero asking prices and zero cap rates remain valid values; zero building area never causes division by zero.

### Input settings

| Setting | Default | Details |
| --- | --- | --- |
| `states` | `["TX"]` | Two-letter US state or territory codes; search mode only |
| `propertyTypes` | `["Industrial", "Retail"]` | Crexi categories, including Industrial, Retail, Multifamily, Office, Land, and Mixed Use |
| `minCapRate` | Unset | Number from 0 to 25; excludes listings with unknown or lower cap rates |
| `maxAskingPrice` | Unset | Nonnegative USD price ceiling; excludes listings with unknown asking prices |
| `directListingUrls` | Unset | Up to 2,000 public HTTP/HTTPS listing URLs; switches to direct mode |
| `maxItems` | `30` | Total saved rows, including errors; range 1–2,000; Console prefill is 3 |
| `timeoutSecs` | `15` | Request deadline including redirects and retries; range 3–60 seconds |
| `maxConcurrency` | `25` | Requested property concurrency, range 1–100; automatically reduced for available memory |
| `proxyConfiguration` | Unset | Optional Apify or custom proxy settings |

Crexi search uses `POST https://api.crexi.com/assets/search` with 60-item pages, offsets, state/type filters, financial filters, and the required `includeUnpriced` flag. Public details and broker endpoints enrich results. Duplicate assets and repeated search pages are detected. Filtered-out listings do not count toward `maxItems`; actual saved `SUCCESS` and `ERROR` rows do.

Connections start directly. On HTTP 403, 429, or 503 the Actor tries the available SHADER datacenter proxy group and then the account's default datacenter proxy. A configured or residential Apify proxy is attempted only when `useApifyProxy` is explicitly `true`. Custom `proxyUrls` are used exclusively; `useApifyProxy: false` without custom URLs forces direct connections. Proxy availability depends on the account and injected proxy credentials. No user-profile API is called, making the Actor suitable for `LIMITED_PERMISSIONS`.

### Output example

Illustrative successful row:

```json
{
  "platform": "Crexi",
  "assetId": "1234567",
  "status": "SUCCESS",
  "propertyName": "Example Industrial Center",
  "propertyType": "Industrial",
  "askingPrice": 2500000,
  "capRatePercent": 7.5,
  "estimatedAnnualNoi": 187500,
  "squareFootage": 20000,
  "pricePerSqFt": 125,
  "lotSizeAcres": 2.5,
  "yearBuilt": 2001,
  "occupancyPercent": 95,
  "tenancyType": "Multi Tenant",
  "streetAddress": "100 Commerce Drive",
  "city": "Dallas",
  "state": "TX",
  "zipCode": "75201",
  "brokerName": "Jordan Example",
  "brokerCompany": "Example Commercial",
  "brokerPhone": "+1 (214) 555-0100",
  "listingUrl": "https://www.crexi.com/properties/1234567/texas-example-industrial-center",
  "scrapedAt": "2026-10-19T12:00:00.000Z",
  "errorMessage": null
}
```

Failed targets preserve the same flat schema, with `status: "ERROR"`, unavailable fields set to `null`, the attempted URL, and a safe message such as `[example.com] HTTP 403 [HTTP_403].` A failed detail or broker request does not discard valid search data. If the search request itself fails, the Actor saves one uncharged `ERROR` row identifying the search endpoint and stops gracefully.

### Use the results in Clay, n8n, Make, or your API

- **Clay:** Import the dataset CSV, map `listingUrl` as the property key, and use address and broker fields in your acquisition workflow.
- **n8n:** Run the Actor with an Apify node, wait for completion, and fetch dataset items. Route `SUCCESS` rows into your CRM and `ERROR` rows into a retry workflow.
- **Make:** Use Apify's Run an Actor and Get Dataset Items modules to schedule searches and synchronize selected properties.
- **API:** Start the Actor using Apify's run API, read `defaultDatasetId` from the run, and fetch `/v2/datasets/{datasetId}/items?format=json&clean=true`. Authenticate through your integration's secret store. Use webhooks or polling to wait for run completion.

See the official [Actor run API](https://docs.apify.com/api/v2/actors-runs-post) and [dataset items API](https://docs.apify.com/api/v2/dataset-items-get) for integration details.

### Pricing and limits

The custom pay-per-event event is **`property-scraped`**. Only saved `SUCCESS` rows use that event. **`ERROR` rows cost $0 in event fees** and still count toward the saved-row limit. Compute, storage, and authorized proxy usage may have separate Apify costs.

Concrete example: if the Store price is **$2 per 1,000 successful properties** ($0.002 per event), 1,000 successful properties cost $2 in custom event fees. A run saving 900 successes and 100 errors costs $1.80 in custom event fees at that rate. This is an illustration; the current Store price controls actual charges.

Free Apify accounts are capped at 25 total saved rows per run. Paid accounts and local runs support the requested `maxItems`, up to 2,000. When the event budget is exhausted, new work stops, already buffered eligible results are saved, and the run ends cleanly. Larger searches can finish with fewer items when the source runs out of listings or financial filters exclude unavailable values.

For deployment, configure the `property-scraped` event and its price in the Apify Console monetization settings. Keep automatic default-dataset-item event pricing disabled so error rows stay free of event charges; the Actor rejects that incompatible pricing setup with a clear status. Local repository files do not activate Store monetization. See [Apify pay-per-event documentation](https://docs.apify.com/sdk/js/docs/concepts/pay-per-event).

### FAQ and troubleshooting

**Why are cap rates, NOI, year built, or phone numbers missing?**

Crexi and other sources may mask those fields, omit them from public endpoints, or require account access. The Actor returns `null` and never attempts to reconstruct protected values. A cap rate explicitly published in a listing description can still be extracted. Public asking price and cap rate can supply an estimated NOI.

**Why are there fewer results after setting a financial filter?**

Unknown values cannot satisfy a financial filter. State and type filters are rechecked on search results, and financial filters are checked on both search and direct results. A successful fetch can therefore produce no saved row when its data does not meet the filters.

**What happens on HTTP blocks, malformed responses, or timeouts?**

The Actor applies the permitted proxy cascade and protocol fallback. Unreachable targets become uncharged `ERROR` rows. Valid search data remains usable if enrichment fails. The run exits cleanly on individual source failures; invalid user input exits with code 1. A successful run status alone does not imply that every row succeeded; inspect `status` in the dataset.

**Can partially downloaded pages still succeed?**

Yes, if a successful HTTP response has already supplied at least 16 KiB of HTML before a timeout or socket reset and the prefix contains usable listing information. HTML responses are capped at 4 MiB after decompression; oversized HTML can also use its parsed prefix. JSON is capped at 2 MiB and is never salvaged partially. The internal client tracks truncation without adding fields to the specified dataset schema.

**How are requests protected?**

All target and redirect URLs use public HTTP/HTTPS endpoints on default ports. Every DNS answer must be public. Validated addresses are pinned while preserving the target hostname and TLS certificate checks. Proxy tunnels also use pinned destination addresses. Local networks, metadata services, unsafe protocols, URL credentials, HTTPS downgrades, and excessive redirects are blocked. Parsers limit nesting and text windows, ignore external entities, and remove prototype-pollution keys. Dataset strings are protected against CSV formulas while valid phone numbers remain readable.

**How do I build and test locally?**

Use Node.js 22 or later:

```sh
npm ci --ignore-scripts
npm run build
npm test
npm run smoke
npm start
```

`npm test` uses `node:test` with mocked networking and covers SSRF, DNS pinning, redirects, proxy policy, byte limits, interrupted streams, parser accuracy, billing, free-plan caps, schemas, and pagination. `npm run smoke` performs a real direct Texas industrial search for three items, prints timings and memory usage, and does not initialize Apify billing or storage. `npm start` runs the complete Actor and uses the standard local Apify storage input. No environment file is loaded by the repository scripts.

The Docker image uses a multi-stage `node:22-alpine` build and runs as a non-root user with 256 MiB as the default Apify allocation. `UV_THREADPOOL_SIZE=64` and production mode are set before the application and test child processes start. Result writes are serialized, grouped into batches of 100 or flushed every five seconds. SIGINT, SIGTERM, and Apify abort events cancel pending requests and flush accepted rows.

Repository initialized by Arezgit.

# Actor input Schema

## `states` (type: `array`):

Two-letter US state codes for Crexi search. Direct listing URLs take priority over search.

## `propertyTypes` (type: `array`):

Crexi property types, such as Industrial, Retail, Multifamily, Office, Land, or Mixed Use. Used for search mode.

## `minCapRate` (type: `number`):

Minimum published cap rate, in percentage points. Listings without a public cap rate are excluded when this filter is set.

## `maxAskingPrice` (type: `number`):

Maximum published asking price in USD. Listings without a public asking price are excluded when this filter is set.

## `directListingUrls` (type: `array`):

Optional public Crexi or commercial property listing URLs. Providing URLs switches to direct mode; state and property-type search filters are then ignored. Financial filters still apply.

## `maxItems` (type: `integer`):

Maximum total SUCCESS and ERROR rows. Free Apify accounts are limited to 25 rows per run.

## `timeoutSecs` (type: `integer`):

Total deadline for one request, including redirects and proxy fallback attempts.

## `maxConcurrency` (type: `integer`):

Upper bound on concurrent properties. The Actor reduces this automatically to stay within the memory allocation.

## `proxyConfiguration` (type: `object`):

Starts directly, then tries available datacenter proxies after HTTP 403, 429, or 503. Residential or configured Apify proxies require explicit opt-in. Custom proxy URLs are used exclusively; useApifyProxy:false forces direct connections.

## Actor input object example

```json
{
  "states": [
    "TX"
  ],
  "propertyTypes": [
    "Industrial"
  ],
  "maxItems": 3,
  "timeoutSecs": 15,
  "maxConcurrency": 25
}
```

# Actor output Schema

## `results` (type: `string`):

Default dataset items. Review each record status before using extracted data downstream.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "states": [
        "TX"
    ],
    "propertyTypes": [
        "Industrial"
    ],
    "maxItems": 3
};

// Run the Actor and wait for it to finish
const run = await client.actor("arezdev/commercial-real-estate-cap-rate-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "states": ["TX"],
    "propertyTypes": ["Industrial"],
    "maxItems": 3,
}

# Run the Actor and wait for it to finish
run = client.actor("arezdev/commercial-real-estate-cap-rate-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "states": [
    "TX"
  ],
  "propertyTypes": [
    "Industrial"
  ],
  "maxItems": 3
}' |
apify call arezdev/commercial-real-estate-cap-rate-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,arezdev/commercial-real-estate-cap-rate-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/p3BrrpKymvk4ClLSo/builds/RASNdLTqWtfp9LaVp/openapi.json
