# StreetEasy Scraper - NYC Listings, Prices & Agents (`abotapi/streeteasy-scraper`) Actor

Scrape StreetEasy sales, rentals and in-contract listings across NYC and Jersey City. Search with filters or URLs and extract prices, GPS coordinates, photos, amenities, building details, brokerages, agent contacts and price history.

- **URL**: https://apify.com/abotapi/streeteasy-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Categories:** Real estate, Automation, Developer tools
- **Features:** Limited permissions
- **Stats:** 42 total users, 11 monthly users, 85.5% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## StreetEasy Scraper: NYC and Jersey City Listings, Prices, and Price History

StreetEasy Scraper turns StreetEasy into structured, exportable property data. Combine filters, price, beds, baths, square feet, amenities, no-fee, keywords, and sort order into one search, or paste a StreetEasy search-result URL and let the actor walk pagination forward. Covers for-sale, for-rent, and in-contract listings across all five NYC boroughs plus Jersey City and Hoboken, with optional detail-page enrichment for full descriptions, amenities, building info, agent contacts, price history, and open house dates. Export to JSON, CSV, or Excel, read results through the Apify API, or pipe them into your own app.

### Why This Scraper?

- **Two ways in.** Combine filters (price, beds, baths, square feet, amenities, no-fee, keywords, sort, and three listing categories) into one search, or paste StreetEasy search-result URLs, including their own price, beds, and sort path segments.
- **Big pages.** Up to 500 listings per page, so a large pull needs fewer requests.
- **Coordinates included.** Latitude and longitude on every listing without detail-page enrichment.
- **Full-market coverage.** Auto-expand mode splits a search above roughly 1,050 matching results into price buckets and merges them.
- **Optional enrichment.** Detail pages add the full description, verified amenities, building name and year, price history, agent contacts, and open house dates.
- **Rental concessions.** Net effective rent, months free, lease term, and move-in date for concession-aware rental analysis.
- **Incremental mode.** For scheduled runs, get back only listings that are new, updated, reappeared, or gone since the last run.

### Use Cases

- **Real estate investors and analysts:** track price per square foot and price history to spot pricing trends across buildings or neighborhoods.
- **Renters and apartment hunters:** filter by no-fee, amenities, and neighborhood to shortlist rentals without paging through search results.
- **PropTech and market research teams:** build a recurring dataset of NYC and Jersey City listings for pricing models and market reports.
- **Brokerages and agencies:** monitor a set of buildings or neighborhoods for new listings and price changes.
- **Content and news teams:** source current listing data for neighborhood market coverage.

### Data You Get

> Sample shape, values are illustrative placeholders, not from a live listing.

| Field | Example |
| --- | --- |
| `id` | `"0000001"` |
| `url` | `"https://streeteasy.com/building/sample-tower/4a"` |
| `saleType` / `status` | `"forSale"` / `"For sale"` |
| `price` | `1250000` |
| `neighborhood` / `borough` | `"Williamsburg"` / `"Brooklyn"` (borough is a coordinate-based estimate, see FAQ) |
| `address` / `unit` | `"100 Sample Street"` / `"4A"` |
| `bedrooms` / `bathrooms` / `halfBathrooms` | `2` / `2` / `0` |
| `squareFeet` / `pricePerSqft` | `1100` / `1136.36` |
| `propertyType` | `"Condo"` |
| `latitude` / `longitude` | `40.7000` / `-74.0000` |
| `imageUrl` / `images` | a listing photo URL / array of photo URLs |
| `brokerage` | `"Sample Realty"` |
| `listedAt` / `daysOnMarket` | `"2026-01-01"` / `30` (filled with Fetch detail pages on, `null` otherwise) |
| `zipCode` / `state` | `"10011"` / `"NY"` |
| `mediaAssetCount` / `hasVideos` / `hasTour3d` | `12` / `false` / `false` |
| `priceDelta` / `priceChangedAt` | last recorded price change and its date, `null` if never changed |
| `netEffectivePrice` / `monthsFree` / `leaseTerm` / `availableAt` | rentals only: rent after a free-month concession, months free, lease length, move-in date; `null` on sales |
| `description` / `amenities` | (when Fetch detail pages is on) full listing description and a normalized amenity list |
| `buildingName` / `buildingYear` | (when Fetch detail pages is on, and the site's page exposes them) |
| `priceHistory` | (when Fetch detail pages is on) list of `{date, event, price}` entries |
| `agents` | (when Fetch detail pages is on) one object per listing agent: `name`, `phone`, `brokerage`, `license` (license type), `profileId`, `profileUrl`, `contactId`, `isPro`, `headshotUrl`, and the office `businessName` plus `businessAddress1`, `businessAddress2`, `businessCity`, `businessState`, `businessZip`, `fullBusinessAddress`. Sale listings also carry `email`, `agentId` and (when the agent wrote one) `bio` as plain text; rentals expose a phone but no email. A key is simply absent when the page does not ship it. |
| `openHouseDates` / `virtualTourUrl` | (when Fetch detail pages is on) upcoming open house dates, and the 3D tour link when the listing publishes one (otherwise `null`) |
| `buildingId` / `areaId` / `residentialUnitCount` / `roomCount` / `nearbySchools` | (when Fetch detail pages is on) StreetEasy building id, neighborhood area id, residential units in the building, rooms in the unit, and nearby schools as `{id, name, district, grades}` |
| `statusDate` / `verifiedUpToDate` / `salesStartDate` | (when Fetch detail pages is on) date of the latest status change, whether the listing is marked verified up to date (sales), and the sales start date (new developments only, otherwise `null`) |

With `incrementalMode` on, every row also carries `changeType`, `changedFields`, `firstSeenAt`, and `lastSeenAt` (see Output Example).

### How to Use

1. Pick a **mode**: `search` (build a query from filters) or `url` (paste one or more StreetEasy search-result URLs).
2. In search mode, choose **Listing type** and **Locations**. In url mode, paste the URLs. The Filters section (price, beds, baths, square feet, amenities, no-fee, keywords, sort) applies to both modes.
3. Set **Max listings**, and if needed **Max pages** and **Fetch detail pages**, then click **Start**.
4. Download the dataset as JSON, CSV, or Excel, or read it through the API.

**Search mode, single borough:**

```json
{
  "mode": "search",
  "saleType": "forSale",
  "locations": ["manhattan"],
  "minPrice": 1000000,
  "maxPrice": 2000000,
  "minBedrooms": 2,
  "amenities": ["DOORMAN", "ELEVATOR"],
  "sortBy": "PRICE_ASC",
  "maxPages": 5,
  "maxListings": 200
}
```

**Search mode, multi-borough no-fee rentals:**

```json
{
  "mode": "search",
  "saleType": "forRent",
  "locations": ["brooklyn", "queens"],
  "maxPrice": 4000,
  "noFee": true,
  "minBedrooms": 1,
  "sortBy": "LISTED_AT_DESC",
  "maxListings": 100
}
```

**URL shapes (URL mode):**

| Shape | Example | Supported |
|---|---|---|
| Rent, neighborhood + price filter | `https://streeteasy.com/for-rent/williamsburg/price:3000-5000` | yes |
| Sale, city + sort | `https://streeteasy.com/for-sale/nyc/sort:price_asc` | yes (`price_asc`, `price_desc`, `listed_desc`, `listed_asc`, `sqft_desc`, `beds_desc`) |
| Sale, borough + beds | `https://streeteasy.com/for-sale/manhattan/beds:2` | yes (`beds:2`, `beds:1-3`, `beds:2-`) |
| Plain search, `?page=N`, in-contract | `https://streeteasy.com/for-sale/williamsburg`, `...?page=2`, `https://streeteasy.com/in-contract/nyc` | yes (each returned rows in a 2026-10-15 run on Apify Residential US; StreetEasy turns some individual requests away, so a request may be retried) |
| Building detail page | `https://streeteasy.com/building/one-essex-crossing` | no, skipped with a warning ("building detail pages are not supported; paste a for-sale/for-rent/in-contract search URL instead") |
| Sold page | `https://streeteasy.com/sold/williamsburg` | no, skipped with a warning ("sold pages are not supported") |
| Single-listing page (`/rental/...`, `/sale/...`) | | no, skipped with a warning (paste the search URL that contains the listing) |
| Agent profile page (`/profile/...`) | | no, skipped with a warning (agent name, phone, email, licence type, brokerage, profile link and office address ride on each listing row when Fetch detail pages is on) |

Filters can be joined with `|` in one segment (`price:3000-5000|beds:2`). Unrecognized path segments are logged as a warning and ignored (results are not narrowed by them). A `sort:` segment in a pasted URL overrides the **Sort order** field; a `beds:` or `price:` segment overrides the matching Min/Max field bound by bound (a URL with only a minimum still takes your maximum from the field). A URL starting at `?page=N` with N above 1 never saw pages 1 to N-1, so in incremental mode it never marks listings `EXPIRED`.

**Behavior change:** `urls` now defaults to the first three example URLs above (rent price, sale sort, sale beds). Running URL mode with the field left untouched scrapes those three searches, up to your Max listings, instead of nothing.

**URL mode, multiple URLs:**

```json
{
  "mode": "url",
  "urls": [
    "https://streeteasy.com/for-sale/williamsburg",
    "https://streeteasy.com/for-rent/upper-east-side?page=2",
    "https://streeteasy.com/for-sale/manhattan/price:900000-1500000"
  ],
  "maxPages": 3,
  "perPage": 200
}
```

**Detail enrichment (full description, amenities, agent contacts, price history):**

```json
{
  "mode": "search",
  "saleType": "forSale",
  "locations": ["dumbo"],
  "fetchDetails": true,
  "maxListings": 25
}
```

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/streeteasy-scraper").call(run_input={
    "mode": "search",
    "saleType": "forRent",
    "locations": ["brooklyn"],
    "maxListings": 50,
})
for listing in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(listing["address"], listing["price"])
```

JavaScript:

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

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('abotapi/streeteasy-scraper').call({
    mode: 'search',
    saleType: 'forRent',
    locations: ['brooklyn'],
    maxListings: 50,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

Or connect it to Make, Zapier, n8n, Google Sheets, or webhooks from the **Integrations** tab.

#### Auto-expand for large searches

A search matching more than roughly 1,050 listings hits the site's own per-query cap. Turn on **Auto-expand queries above 1050 results** and the actor splits the search into price buckets, runs each one, and merges the results, so you still get the full market instead of just the first 1,050.

#### Resume and recurring updates

Two different things, both configurable on the input:

- **`resumeFromRunId`** continues one specific interrupted run, or grows one big catalogue pull across separate runs. Paste a previous run ID or dataset ID and this run appends only listings not already saved there (matched by listing `id`).
- **`incrementalMode`** is for running the same search again and again on a schedule and getting back only what changed. Turn it on and every emitted row carries `changeType` (`NEW`, `UPDATED`, `REAPPEARED`, `EXPIRED`, or `UNCHANGED`), `changedFields`, `firstSeenAt`, and `lastSeenAt`. By default only `NEW`, `UPDATED`, and `REAPPEARED` rows are returned and billed; turn on `emitUnchanged` or `emitExpired` to also get those (both billed). A listing is `UPDATED` when any compared field changes; these fields are kept in the output but never drive `UPDATED`: daysOnMarket, zipCode, state, mediaAssetCount, hasTour3d, hasVideos, isNewDevelopment, priceDelta, priceChangedAt, netEffectivePrice, monthsFree, leaseTerm, availableAt, buildingId, areaId, residentialUnitCount, roomCount, salesStartDate, statusDate, verifiedUpToDate, nearbySchools, and the agent profile keys (agentId, contactId, profileId, profileUrl, bio, isPro, headshotUrl, business address keys); an agent's name, phone, email, license and brokerage still count (use `ignoreFieldsForChanges` to add your own). `stateKey` names or shares the stored state; leaving it empty derives one automatically from the search scope. For example, `incrementalMode: true` with `locations: ["park-slope"]`, `saleType: "forSale"`, and `stateKey: "park-slope-daily"` tracks that one search across scheduled runs.

#### Output views

The dataset has four views, plus a run summary:

- **overview**: the main listing columns, including `changeType` and the other incremental fields when incremental mode is on.
- **detail**: description, amenities, building, price history, agents, open house dates, building ids, unit/room counts, status date, and nearby schools (filled when Fetch detail pages is on).
- **changes**: `id`, `url`, `address`, `price`, `changeType`, `changedFields`, `firstSeenAt`, `lastSeenAt`, for incremental runs.
- **agents**: one row per listing agent (`name`, `phone`, `email`, `license`, `brokerage`, `profileUrl`, `isPro`, `bio`, `businessName`, `fullBusinessAddress`), unwound from the listing; the full agent objects stay in the `agents` array of each row.
- **Run summary** (key-value store record `OUTPUT`): `totalEmitted`, per-query status (`complete`, `partial`, or `blocked`), and an `incomplete` flag.

### Send results into your apps (MCP connectors)

Optionally pipe the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. This is an extra delivery step **after** the scrape, the Apify dataset is never changed.

**What gets written to the connector:** a condensed, human-readable **summary** of each record, not the full JSON. Each item becomes one entry with a **title** and its key fields flattened to plain text. The **complete record always stays in the Apify dataset**.

1. Authorize a connector once under **Apify → Settings → Integrations** (Notion, Linear, Airtable, or Apify).
2. Select it in the **"Pipe results into your apps"** input field. (If the picker is empty, you haven't authorized a connector yet.)
3. For **Notion**, also set `notionParentPageUrl` to the page where items should be created.

The connection is mediated by Apify's MCP proxy, so this actor never sees your third-party credentials. Leave the field empty to skip.

### Input Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `mode` | string | `search` | `search` (build a query from filters) or `url` (paste search-result URLs). |
| `saleType` | string | `forSale` | `forSale`, `forRent`, or `inContract`. Search mode only. Past sales surface as `priceHistory` events on a listing found this way (with Fetch detail pages on), not as a separate queryable category. |
| `locations` | array | `["nyc"]` | Borough/neighborhood slugs (e.g. `manhattan`, `williamsburg`, `jersey-city`) or numeric area ids. Search mode only. |
| `urls` | array | three example URLs (see URL shapes) | StreetEasy search-result URLs. URL mode only. A URL's own `/price:LOW-HIGH`, `/beds:N` (or `beds:MIN-MAX`) and `/sort:KEY` path segments are honored. Building/single-listing detail URLs and `/sold/...` URLs are not supported and are skipped with a warning. |
| `propertyTypes` | array | `[]` | `CONDO`, `CO_OP`, `HOUSE`, `TOWNHOUSE`, `MULTI_FAMILY`, `RENTAL`, `CONDOP`. Applies to both modes. `RENTAL` is accepted here but currently has no effect on results; use `saleType: forRent` to search rentals instead. |
| `minPrice` | integer | none | Lower bound on price (USD). For sales, total price; for rentals, monthly rent. |
| `maxPrice` | integer | none | Upper bound on price (USD). |
| `minBedrooms` | integer | none | Use `0` for studios. |
| `maxBedrooms` | integer | none | Upper bound on bedroom count. |
| `minBathrooms` | number | none | Lower bound on bathroom count. Decimals supported (e.g. `1.5`). |
| `minSqft` | integer | none | Lower bound on living area (square feet). |
| `maxSqft` | integer | none | Upper bound on living area (square feet). |
| `amenities` | array | `[]` | Listings must include every selected amenity. |
| `noFee` | boolean | `false` | Rentals only: filter to no-broker-fee listings. Ignored when `saleType` is not `forRent`. |
| `keywords` | string | none | Free-text filter matched against the listing description/title. |
| `sortBy` | string | `RECOMMENDED` | `RECOMMENDED`, `PRICE_ASC`, `PRICE_DESC`, `LISTED_AT_DESC`, `LISTED_AT_ASC`, `SQFT_DESC`, `BEDROOMS_DESC`. |
| `maxPages` | integer | `0` | Pagination cap per query. `0` walks every result page until the site's own end of results, bounded only by `maxListings`. |
| `perPage` | integer | `100` | Listings per page, `10` to `500`. Higher values mean fewer round trips. |
| `maxListings` | integer | `20` | Hard cap across the whole run. `0` means unlimited within budget. Raise it for production scrapes. |
| `fetchDetails` | boolean | `false` | Opens each listing's detail page to add description, amenities, building info, agent contacts, and price history. One extra request per listing. |
| `expandLargeQueries` | boolean | `false` | When a search matches more than roughly 1,050 listings, split it into price buckets and merge the results. |
| `proxy` | object | Apify Residential, country US | Apify Residential proxy is required; datacenter ranges are turned away. Country defaults to US when not set. |
| `resumeFromRunId` | string | none | Previous run ID or dataset ID to continue a catalogue pull across separate runs. Listings already saved there (matched by listing `id`) are skipped. |
| `incrementalMode` | boolean | `false` | Recurring, scheduled monitoring of the same search. Off by default, so no existing run changes until you opt in. |
| `stateKey` | string | none | Incremental mode only. Names a monitoring campaign, or deliberately shares state across differently configured runs. Leave empty to derive one automatically from the search scope. |
| `emitUnchanged` | boolean | `false` | Incremental mode only. Also return listings unchanged since the last run, marked `UNCHANGED`. Returns and bills extra rows. |
| `emitExpired` | boolean | `false` | Incremental mode only. Also return listings no longer found, marked `EXPIRED`, but only once a run has fully scanned the tracked search. Returns and bills extra rows. |
| `ignoreFieldsForChanges` | array | `[]` | Incremental mode only. Fields that never count as a change (kept in the output, excluded from `UPDATED` and `changedFields`). Use a top-level name or a dot path such as `agents.phone`; arrays apply to every element. Adding an entry does not cause a one-time wave of `UPDATED` rows. |
| `mcpConnectors` | array | none | Send a condensed summary of each record to apps authorized under Apify → Settings → Integrations (Notion, Linear, Airtable, or Apify). |
| `notionParentPageUrl` | string | none | Notion connector only. Page under which item pages are created. |
| `maxNotifyListings` | integer | `50` | Cap on items written to each connector per run. Does not affect the dataset. |

### Output Example

> Sample shape, values are illustrative placeholders, not from a live listing.

```json
{
  "id": "0000001",
  "url": "https://streeteasy.com/building/sample-tower/4a",
  "saleType": "forSale",
  "status": "For sale",
  "price": 1250000,
  "neighborhood": "Sample Neighborhood",
  "borough": null,
  "address": "100 Sample Street",
  "unit": "4A",
  "bedrooms": 2,
  "bathrooms": 2,
  "halfBathrooms": 0,
  "squareFeet": 1100,
  "pricePerSqft": 1136.36,
  "propertyType": "Condo",
  "latitude": 40.7000,
  "longitude": -74.0000,
  "imageUrl": "https://images.streeteasy.com/photos/sample-uncropped.webp",
  "images": ["https://images.streeteasy.com/photos/sample-uncropped.webp"],
  "brokerage": "Sample Realty",
  "listedAt": "2026-01-01",
  "daysOnMarket": 30,
  "offMarketAt": null,
  "furnished": false,
  "noFee": null,
  "advertisedListing": false,
  "description": "Full listing description text appears here when Fetch detail pages is on.",
  "amenities": ["Doorman", "Elevator", "Gym"],
  "buildingName": "Sample Tower",
  "buildingYear": 1990,
  "schoolDistricts": [],
  "transit": ["6", "Q"],
  "priceHistory": [{ "date": "2026-01-01", "event": "Listed", "price": 1250000 }],
  "agents": [{ "name": "Agent Name", "phone": "+10000000000", "email": "agent@example.com", "license": "Licensed Real Estate Salesperson", "brokerage": "Sample Realty", "contactId": "100001", "profileId": "200001", "profileUrl": "https://streeteasy.com/200001", "isPro": false, "businessName": "Sample Realty LLC", "businessAddress1": "1 Example Avenue", "businessCity": "New York", "businessState": "NY", "businessZip": "10001", "fullBusinessAddress": "1 Example Avenue, New York, NY 10001" }],
  "openHouseDates": ["2026-02-01"],
  "virtualTourUrl": null,
  "buildingId": "10001",
  "areaId": 120,
  "residentialUnitCount": 40,
  "roomCount": 4,
  "statusDate": "2026-01-01T09:00:00.000-05:00",
  "verifiedUpToDate": false,
  "salesStartDate": null,
  "nearbySchools": [{ "id": "ps-1-manhattan", "name": "P.S. 001 Sample School", "district": "2", "grades": ["PK", "01", "02"] }],
  "sourceUrl": null,
  "zipCode": "10011",
  "state": "NY",
  "mediaAssetCount": 12,
  "hasTour3d": false,
  "hasVideos": false,
  "isNewDevelopment": false,
  "priceDelta": -25000,
  "priceChangedAt": "2026-01-15T00:00:00.000Z",
  "netEffectivePrice": null,
  "monthsFree": null,
  "leaseTerm": null,
  "availableAt": null
}
```

`borough` is always a coordinate-based estimate, filled in whether or not Fetch detail pages is on (it is `null` only when the listing carries no usable coordinates). `schoolDistricts` is always `[]` (the school list is in `nearbySchools`), and `virtualTourUrl` is `null` unless the listing publishes a 3D tour. With `incrementalMode` on, every row also carries `changeType` (one of `NEW`, `UPDATED`, `REAPPEARED`, `UNCHANGED`, or `EXPIRED`), `changedFields` (an array of field names), `firstSeenAt`, and `lastSeenAt`; these four keys are absent entirely when `incrementalMode` is off.

### Plan Requirement

This actor needs Apify's Residential proxy group, country US. StreetEasy turns away datacenter IP ranges with an "Access to this page has been denied" page (HTTP 403) in our measurements, so a datacenter proxy returns no listings. Even on Residential, some individual requests are turned away and the actor retries them; this varies by connection and by page, so a very large run can occasionally end partial.

Residential proxy availability depends on your Apify plan; Apify free plans do not include it. What you will see in the log and run status:

- Plan without Residential: a warning block reads "The selected connection is unavailable to this run. Apify free plans do not include residential proxies. Continuing on a backup connection; some pages may still be unavailable." When pages are served that way, the log says "Served over the backup connection (the default connection was unavailable)."
- If the backup connection cannot load the pages either, the run ends FAILED with: "No results: residential proxies are unavailable on this run (Apify free plans do not include them), and the backup connection could not load the requested StreetEasy pages. Upgrade at https://apify.com/pricing or supply your own proxy URLs, then try again."
- If the proxy connection is refused for the account, the run ends FAILED with: "The proxy connection was refused for this account, so no page could be read. Your Apify plan does not include proxy access; a paid plan is required: https://apify.com/pricing."
- If no results page could be loaded at all, the run ends FAILED with: "Could not load any results page (the requests failed), so nothing was collected. This is a connection problem, not an empty search. Please try again later."
- If a page is turned away after some listings were already collected, the log warns "Results are INCOMPLETE: page N of ... was turned away after X listing(s) had been collected, so the remaining pages were skipped." and the run summary marks that query `partial` with `incomplete: true`.

#### Pricing

Pay per event: a start event of $0.08 per run, $0.002 per listing returned, and $0.0008 per listing whose detail page was actually fetched (only when Fetch detail pages is on). The Pricing tab shows the current rates; use **Max listings** to cap a run. In incremental mode only the rows returned are billed.

### FAQ

#### How much does it cost?

You pay per listing returned, plus an optional surcharge that applies only when Fetch detail pages is switched on, and only for listings that actually returned a detail page. Current rates: $0.08 per run start, $0.002 per listing, $0.0008 per detail page (see Pricing above). Use **Max listings** to cap the cost of any run.

#### Is it legal to scrape StreetEasy?

This actor collects only publicly available listing data. You are responsible for how you use it: follow StreetEasy's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution. Listing facts such as price and square footage are generally data, but photos and descriptions may carry their own rights.

#### Can I get only new or changed listings on a schedule?

Yes. Schedule the actor from the **Schedules** tab and turn on **Incremental mode** for the search you want to monitor. Each row is classified by comparing a stable fingerprint of the listing's fields against the previous run's state, so a listing with no real content change is marked `UNCHANGED` and, by default, skipped and not billed; a genuine change is marked `UPDATED` with the changed fields listed.

#### Why is `borough` sometimes null?

`borough` is estimated from the listing's coordinates, with or without Fetch detail pages. It is `null` only when the listing has no usable coordinates or they fall outside the five boroughs plus Jersey City and Hoboken. `neighborhood` and `address` always come straight from the listing. If you track a search with **Incremental mode** and your saved state was taken before `borough` was always filled in, the first run after the upgrade treats a row whose only difference is `borough` going from empty to a value as `UNCHANGED`: it is not emitted and not billed.

#### Why did my run fail instead of returning an empty dataset?

The run only fails when the site turned away every request it made (search pages or the location lookup), which is a connection problem, not an empty search. Check the Proxy setting and try again. If your filters simply match nothing, the run finishes with an empty dataset and a message in the log. A run without Residential proxy continues on a backup connection (you will see the warning described under Plan Requirement) and ends FAILED only when no page could be loaded at all, with the message "Could not load any results page (the requests failed), so nothing was collected. This is a connection problem, not an empty search. Please try again later."

#### Can I use it with AI agents or MCP?

Yes. Call it from any Apify integration or MCP client, and use the connector field to push a summary of each listing into Notion, Linear, or Airtable.

### 🔗 Want more real estate data?

Pair this actor with these related scrapers from the same team:

<table>
<tr><td>🏠 <a href="https://apify.com/abotapi/zumper-scraper"><b>Zumper Scraper</b></a><br>Scrape Zumper.com rental listings: apartments, houses, condos and rooms. Pulls 60+ fields...</td><td>🧩 <a href="https://apify.com/abotapi/padmapper-rental-listings-scraper"><b>Padmapper Scraper</b></a><br>Scrape Padmapper apartment rentals across US/CA cities: rent, beds, baths, sqft...</td></tr>
<tr><td>🏠 <a href="https://apify.com/abotapi/trulia-scraper"><b>Trulia Scraper</b></a><br>Scrape Trulia.com property listings by location, filters, or URL. Extract prices, beds...</td><td>🏠 <a href="https://apify.com/abotapi/realtor-com-scraper"><b>Realtor.com</b></a><br>From $0.9/1K. Extract property listings from realtor.com. Get comprehensive data...</td></tr>
<tr><td>🏠 <a href="https://apify.com/abotapi/rent-com-scraper"><b>Rent.com Scraper</b></a><br>Extract rental listings from rent.com. Get comprehensive data including monthly rent...</td><td>🏠 <a href="https://apify.com/abotapi/apartments-com-scraper"><b>Apartments.com Scraper</b></a><br>From $1.50/1K. Scrape Apartments.com rentals by city, ZIP, neighborhood, address or URL...</td></tr>
</table>

👉 [Browse all abotapi scrapers](https://apify.com/abotapi)

### 💬 Support

- 🐞 **Found a bug or a missing field?** Open a ticket on the [Issues tab](https://apify.com/abotapi/streeteasy-scraper/issues/open). We usually reply within hours.
- 🛠️ **Need another site, extra fields or a private build?** Email <contact@abotapi.com>.
- ⭐ **Enjoying it?** A quick review on the actor page helps other users find it.

# Actor input Schema

## `mode` (type: `string`):

Use search mode to combine filters (areas, price, beds, baths, etc.) into one query. Use url mode to paste one or more StreetEasy search-result URLs (for-sale, for-rent, in-contract) and let the actor walk pagination forward. The Filters section applies in both modes.

## `saleType` (type: `string`):

For sale, for rent, or in contract. StreetEasy no longer serves a standalone recently-sold or off-market browse page, so past sales come back as price-history events on a listing (turn on Fetch detail pages), not as a category you can query here.

## `locations` (type: `array`):

Slugs as they appear in StreetEasy URLs. Examples: nyc (all NYC), manhattan, brooklyn, queens, bronx, staten-island, williamsburg, astoria, jersey-city, hoboken, upper-east-side, park-slope, dumbo. Multi-select supported. Defaults to all NYC.

## `urls` (type: `array`):

Examples:
• https://streeteasy.com/for-sale/manhattan
• https://streeteasy.com/for-rent/williamsburg/price:3000-5000
• https://streeteasy.com/for-sale/nyc/sort:price_asc
• https://streeteasy.com/for-sale/manhattan/beds:2
A URL's own price:LOW-HIGH, beds:N (or beds:MIN-MAX) and sort:KEY path segments, if present, are honored (join with | in one segment, e.g. price:3000-5000|beds:2). Unrecognized path segments are logged and ignored. Leaving this field untouched in URL mode scrapes the three examples above. Not supported (skipped with a warning): a building/single-listing detail page URL, or a /sold/... URL (StreetEasy no longer serves a standalone recently-sold browse page; see saleType above for what this actor can query). Multi-URL supported. Pagination starts at the URL's page and walks forward.
The Filters section below also applies to a pasted URL, and narrows it further: property types, beds, baths, sqft, amenities, no-fee, keywords, sort order, plus Min/Max price whenever the URL itself carries no price segment. Only the Search mode fields above (Listing type, Locations) are unused here, because the pasted URL already supplies them.

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

Filter by building / property type. Multi-select supported.

## `minPrice` (type: `integer`):

Lower bound on price. For sales: total price. For rentals: monthly rent.

## `maxPrice` (type: `integer`):

Upper bound on price. For sales: total price. For rentals: monthly rent.

## `minBedrooms` (type: `integer`):

Use 0 for studios.

## `maxBedrooms` (type: `integer`):

Upper bound on bedroom count.

## `minBathrooms` (type: `number`):

Lower bound on bathroom count. Decimal supported (1.5).

## `minSqft` (type: `integer`):

Lower bound on living area (square feet).

## `maxSqft` (type: `integer`):

Upper bound on living area (square feet).

## `amenities` (type: `array`):

Filter to listings that advertise these amenities.

## `noFee` (type: `boolean`):

Rentals: only listings with no broker fee. Ignored when saleType is not forRent.

## `keywords` (type: `string`):

Free-text keyword filter (matches description / title).

## `sortBy` (type: `string`):

Order in which results are returned.

## `maxPages` (type: `integer`):

Pagination cap per query. Each page returns up to perPage listings (default 100). Leave empty to walk every result page, bounded only by maxListings.

## `perPage` (type: `integer`):

Up to 500 supported. Higher values mean fewer round trips and lower cost.

## `maxListings` (type: `integer`):

Hard cap across the whole run. The actor stops as soon as this many records have been pushed. Default 20 keeps small test runs cheap; bump it for production scrapes.

## `fetchDetails` (type: `boolean`):

When on, the actor opens each listing page to enrich amenities, building info, agent contact, full description, and price history. Adds one HTTP request per listing.

## `expandLargeQueries` (type: `boolean`):

When on and a query matches more than the per-query cap (~1050), the actor splits it into price buckets and merges results so you can extract the full market.

## `proxy` (type: `object`):

Apify Residential US is required (StreetEasy blocks datacenter ranges). Country is forced to US.

## `resumeFromRunId` (type: `string`):

Paste a previous run ID or dataset ID to continue a full-catalogue walk across separate runs. Listings already collected there (matched by listing id) are skipped, so this run only saves the new ones. Use this after an interrupted run, or to keep growing one big catalogue pull. For recurring daily/weekly monitoring of the same search, use Incremental mode below instead — the two are different things and normally not combined.

## `incrementalMode` (type: `boolean`):

Turn this on for daily/weekly monitoring of the same search. The first run returns every matching listing as NEW. Later runs normally return only NEW, UPDATED, and REAPPEARED listings — unchanged ones are skipped (and not billed). A listing is UPDATED when any compared field changes (price, status, address, unit, bedrooms, bathrooms, size, images, amenities, open houses, detail-page fields and so on); changedFields names them. These counters and derived fields are kept in the output but never drive UPDATED: daysOnMarket, zipCode, state, mediaAssetCount, hasTour3d, hasVideos, isNewDevelopment, priceDelta, priceChangedAt, netEffectivePrice, monthsFree, leaseTerm, availableAt. State is kept separately per search scope (mode, location/URL, filters, and the Fetch detail pages setting); use State key below to name or deliberately share a monitoring campaign. To continue one specific interrupted run instead, use Resume from a previous run above.

## `stateKey` (type: `string`):

Optional. Name this monitoring campaign to keep its state stable, or to deliberately share state across differently-configured runs. Leave empty to let the actor derive a key automatically from the search scope (locations/URLs, filters, and Fetch detail pages) — different searches then never mix state with each other.

## `emitUnchanged` (type: `boolean`):

Off by default. Turn on to also return listings that have not changed since the last run, marked UNCHANGED. This returns — and bills — extra rows you already have, so leave it off unless you specifically want the full snapshot every run.

## `emitExpired` (type: `boolean`):

Off by default. Turn on to also return listings that were present in a previous run but are no longer found, marked EXPIRED. Only produced once a run has fully scanned the tracked search — not when Max listings/Max pages capped it, the site blocked a page, or Resume was used. This returns — and bills — extra synthetic rows, so leave it off unless you need expiry tracking.

## `ignoreFieldsForChanges` (type: `array`):

Optional list of fields that must not count as a change. They stay in the output but never make a listing UPDATED or appear in changedFields. Use a top-level name (e.g. price) or a dot path for nested fields (e.g. agents.phone; arrays are applied to every element). Applied on top of the built-in ignore list described under Incremental changes. Adding an entry does not cause a one-time wave of UPDATED rows.

## `mcpConnectors` (type: `array`):

Optionally send the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. Authorize a connector once under Apify → Settings → Integrations, then select it here. The connector receives a condensed, human-readable summary per item (title + key fields), not the full JSON — the complete record stays in the dataset. Leave empty to skip. Supported: Notion (https://mcp.notion.com/mcp), Linear (https://mcp.linear.app/sse), Airtable (https://mcp.airtable.com/mcp), Apify (https://mcp.apify.com).

## `notionParentPageUrl` (type: `string`):

URL (or id) of the Notion page under which item pages are created. Required to enable the Notion export; ignored by other connectors.

## `maxNotifyListings` (type: `integer`):

Cap on items written to each connector per run. Does not affect the dataset.

## Actor input object example

```json
{
  "mode": "search",
  "saleType": "forSale",
  "locations": [
    "nyc"
  ],
  "urls": [
    "https://streeteasy.com/for-rent/williamsburg/price:3000-5000",
    "https://streeteasy.com/for-sale/nyc/sort:price_asc",
    "https://streeteasy.com/for-sale/manhattan/beds:2"
  ],
  "propertyTypes": [],
  "amenities": [],
  "noFee": false,
  "sortBy": "RECOMMENDED",
  "maxPages": 0,
  "perPage": 100,
  "maxListings": 20,
  "fetchDetails": false,
  "expandLargeQueries": false,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "ignoreFieldsForChanges": [],
  "maxNotifyListings": 50
}
```

# Actor output Schema

## `overview` (type: `string`):

No description

## `detail` (type: `string`):

No description

## `changes` (type: `string`):

No description

## `agents` (type: `string`):

No description

## `summary` (type: `string`):

No description

# 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 = {
    "mode": "search",
    "locations": [
        "nyc"
    ],
    "urls": [
        "https://streeteasy.com/for-rent/williamsburg/price:3000-5000",
        "https://streeteasy.com/for-sale/nyc/sort:price_asc",
        "https://streeteasy.com/for-sale/manhattan/beds:2"
    ],
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "US"
    },
    "ignoreFieldsForChanges": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/streeteasy-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 = {
    "mode": "search",
    "locations": ["nyc"],
    "urls": [
        "https://streeteasy.com/for-rent/williamsburg/price:3000-5000",
        "https://streeteasy.com/for-sale/nyc/sort:price_asc",
        "https://streeteasy.com/for-sale/manhattan/beds:2",
    ],
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "US",
    },
    "ignoreFieldsForChanges": [],
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/streeteasy-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 '{
  "mode": "search",
  "locations": [
    "nyc"
  ],
  "urls": [
    "https://streeteasy.com/for-rent/williamsburg/price:3000-5000",
    "https://streeteasy.com/for-sale/nyc/sort:price_asc",
    "https://streeteasy.com/for-sale/manhattan/beds:2"
  ],
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  },
  "ignoreFieldsForChanges": []
}' |
apify call abotapi/streeteasy-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,abotapi/streeteasy-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/cilYzUyGRv5K0JZrR/builds/3yTzo2AseEafI4gJc/openapi.json
