# Real Estate Listings & Sold Comps Scraper (`eiv/real-estate-scraper`) Actor

Scrape US property listings and sold comparables from Redfin. Every row carries price per square foot, HOA, lot size, year built, MLS number and coordinates. Sold searches return confirmed sales only - dated and priced, never padded with unconfirmed rows - plus median $/sqft per area.

- **URL**: https://apify.com/eiv/real-estate-scraper.md
- **Developed by:** [Eimantas V](https://apify.com/eiv) (community)
- **Categories:** Real estate, Automation, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.40 / 1,000 property scrapes

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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Real Estate Listings & Sold Comps

For-sale listings **and sold comparables** — with price per square foot, HOA, lot size, year built and coordinates on every row.

```
Austin, TX     350 properties   median $549,000 @ $318/sqft   4 days on market
Miami, FL      348 properties   median $650,000 @ $466/sqft   2 days on market
Phoenix, AZ    349 properties   median $467,000 @ $290/sqft   2 days on market

2704 Winding Brook Dr, Austin 78748   SOLD 2026-06-05   $400,000
3 bed / 2 bath · 1,890 sqft · $212/sqft · built 1996 · HOA $42 · lot 5,793 sqft
```

**1,068 properties in 4 requests and 2.2 seconds.** No browser, no API key, no MLS membership.

***

### Sold comps are the point

Live listings are visible to anyone. What a property **actually sold for** — with a date, a price per square foot and a matching set of beds, baths and floor area — is what valuations, offer analysis and rental underwriting are built on, and it normally sits behind an MLS membership or an enterprise data contract.

Set `listingType: "sold"` and you get exactly that, with a look-back window you choose.

***

### What you get

**Per property**

| Group | Fields |
|---|---|
| **Valuation** | `price`, **`pricePerSqft`**, `squareFeet`, `lotSizeSqft`, `yearBuilt`, `propertyAgeYears` |
| **Comparables** | `isSold`, **`soldDate`**, `daysOnMarket`, `status`, `saleType` |
| **Carrying cost** | **`hoaMonthly`** — the field that decides whether a rental actually cashflows |
| **Location** | `address`, `city`, `state`, `zipCode`, `neighborhood`, **`latitude`**, **`longitude`** |
| **Identity** | `mlsNumber`, `source`, `listingUrl`, open-house times |

**Per location** — median price, **median price per square foot**, median size, median days on market, price range, HOA count, new-build count and `unconfirmedSales`. Medians throughout: one estate moves a mean by six figures and a median not at all.

***

### Who this is for

- **Investors** — pull sold comps for a ZIP, sort by `pricePerSqft`, and find what is mispriced. `hoaMonthly` and `lotSizeSqft` are in the same row, so a yield model needs no second source.
- **Agents and appraisers** — a defensible comp set in one run: same area, same window, same fields.
- **Proptech and iBuyers** — coordinates on every row, MLS number as a stable key, so daily runs diff cleanly.
- **Market researchers** — median $/sqft across dozens of areas, refreshed on a schedule.

***

### Input

```json
{
  "locations": [
    "https://www.redfin.com/city/30818/TX/Austin",
    "https://www.redfin.com/zipcode/78704"
  ],
  "listingType": "sold",
  "soldWithinDays": 180,
  "propertyTypes": ["house"],
  "minPrice": 300000
}
```

Open the area on redfin.com and paste the address bar. City, ZIP, county and neighbourhood pages all work; an individual property page or a saved search does not.

| Option | Default | Notes |
|---|---|---|
| `listingType` | `for-sale` | `sold` for comparables |
| `soldWithinDays` | `90` | Only applies to comparables |
| `maxListingsPerLocation` | `350` | Redfin's own per-export ceiling |
| `propertyTypes` | all | house, condo, townhouse, multi-family, land, manufactured-home, co-op, other |
| `minPrice` / `maxPrice` | — | Also the way around the 350 ceiling — run price bands |
| `minBeds` / `minBaths` / `minSqft` | — | Applied by Redfin before billing |
| `minYearBuilt` | — | Skip pre-1978 stock, or isolate new construction |
| `maxConcurrency` | `5` | Deliberately low; see below |

***

### Four things worth knowing

Each was found by running against live data.

**A sold export is not all sales.** Redfin returns two kinds of row under the same `PAST SALE` label: confirmed sales, with a status of `Sold` and a date, and records it carries an address for but cannot confirm — blank status, no date, and in non-disclosure states such as Texas no price either. The second kind is not rare. It is 153 of 343 rows in Miami, 131 of 350 in Phoenix, and about a tenth of Austin. Their prices are not sale prices, so leaving them in drags the median: Phoenix's median sale price reads **$508,500 with them and $600,000 without**, an 18% error in the exact number a valuation turns on. A sold search here returns confirmed sales only, counts the rest as `unconfirmedSales` on the summary row, and does not bill for them.

**350 properties per location is a hard ceiling, and it is Redfin's.** The export gives no way to page past it. When an area has more, the summary row is flagged `truncated: true` and the run warns. The way through is to split by price band — `$0–400k`, `$400–700k`, `$700k+` — or to use ZIP codes instead of a whole city. Anything claiming unlimited results from this source is paging something else, or not telling you it stopped.

**An empty response is not an empty market.** Redfin sheds load by returning HTTP 200 with no body at all, which is indistinguishable from "no properties here" on the status line and completely different in the body. A genuine empty result still arrives with its CSV header. The Actor checks for that header, retries with a backoff when it is missing, and reports `THROTTLED` rather than quietly telling you a suburb has no houses in it. This is also why `maxConcurrency` defaults to 5.

**A ZIP code is not a region id.** `redfin.com/zipcode/78701` carries the postal code; Redfin's internal id for that area is `34095`. Reading the ZIP as an id queries a different region that returns a valid but empty export — a wrong answer that looks like a real one. Only city URLs carry a usable id in the path; everything else is resolved from the page.

***

### Output

```json
{
  "recordType": "listing",
  "address": "2704 Winding Brook Dr", "city": "Austin", "zipCode": "78748",
  "latitude": 30.150797, "longitude": -97.8535557,
  "price": 400000, "pricePerSqft": 212,
  "beds": 3, "baths": 2, "squareFeet": 1890, "lotSizeSqft": 5793,
  "yearBuilt": 1996, "propertyAgeYears": 30, "hoaMonthly": 42,
  "isSold": true, "soldDate": "2026-06-05",
  "propertyType": "Single Family Residential", "mlsNumber": "…"
}
```

Three ready-made views: **Properties**, **Sold comparables** and **Market summary**. Set `flattenOutput: true` for CSV.

***

### Honest limits

- **United States only.** Redfin does not cover other markets.
- **350 properties per location per request.** See above.
- **`daysOnMarket` is empty on comparables.** Redfin populates it on every for-sale row and on none of the sold ones, so `medianDaysOnMarket` is null for a sold search. Null rather than a guess.
- **A city search spills slightly into neighbouring cities.** An Austin export came back as `Austin: 344, Manor: 1, Cedar Park: 1`. Filter on `city` if you need a strict boundary.
- **Not every field is on every row.** Bathroom counts and lot sizes are missing from some MLS feeds; those arrive as null rather than zero.
- **`pricePerSqft` is derived** when Redfin omits it and both price and floor area are present. It is never invented from one of them.
- **Price bounds on a sold search are re-applied here.** Redfin filters comparables by list price and exports the sale price, so a sale that closed away from asking slips through its filter — measured at ten rows over a $400k ceiling, the highest at $425,000. Those rows are dropped and not charged.
- **Some MLS feeds withhold listings** from bulk export by their own rules, which Redfin states in the file. A location is a near-complete picture, not a guaranteed-complete one.
- **Throttled locations, unresolvable locations and unconfirmed sales are never charged.**

***

### Pricing

| Event | Price | When |
|---|---|---|
| Actor start | $0.005 | Once per run |
| Location scraped | $0.004 | Per location resolved and summarised |
| Property scraped | $0.0004 | Per property returned, after filters |

**$0.40 per 1,000 properties.** Fifty ZIP codes at 350 each costs about **$7.20**. Failures and unconfirmed sales are free.

***

### Tips

- **Run sold comps weekly and diff on `mlsNumber`.** New rows are closings; that is your market pulse before any index publishes it.
- **Median `pricePerSqft` per ZIP, tracked over months,** is the cleanest local price index you can build from public data.
- **Split a hot city into price bands** to get past the 350 ceiling and, incidentally, to see how each band is moving separately.
- **`daysOnMarket` rising while `medianPricePerSqft` holds** is the earliest sign of a market turning — sellers stop selling before they cut.
- **Pair `hoaMonthly` with `pricePerSqft`** when screening rentals. A cheap condo with a $600 HOA is not cheap.

# Actor input Schema

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

One per line. Open the area on redfin.com and paste the address bar: a city page (redfin.com/city/30818/TX/Austin), a ZIP page (redfin.com/zipcode/78704), a county page or a neighbourhood page. Individual property pages and saved searches will not work.

## `startUrls` (type: `array`):

Alternative to Locations, for feeding output from another Actor straight into this one.

## `sourceDatasetId` (type: `string`):

Read location URLs from an existing dataset instead of typing them.

## `sourceDatasetField` (type: `string`):

Which field on the source dataset holds the location URL.

## `listingType` (type: `string`):

'For sale' returns the live market. 'Sold' returns comparables — the same fields plus a sold date — which is what valuations and offer analysis are built on.

## `soldWithinDays` (type: `integer`):

Look-back window for comparables. Ignored when returning for-sale listings. Appraisers usually work to 90 or 180 days.

## `maxListingsPerLocation` (type: `integer`):

Redfin's export returns at most 350 properties per request and gives no way to page past that. When a location has more, the summary row is flagged truncated — narrow with a price range, a property type, or a smaller area.

## `includeLocationSummary` (type: `boolean`):

Add one rollup record per location with median price, median price per square foot, median size and median days on market. Not billed as a property.

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

Leave empty for all. Valid values: house, condo, townhouse, multi-family, land, manufactured-home, co-op, other.

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

In US dollars. 0 disables. On a sold search Redfin filters by list price while the export carries the sale price, so the bounds are re-applied to the sale price here — a comparable that closed outside your range is dropped and not charged.

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

In US dollars. 0 disables. Pairing a min and max is also the way to work around the 350-property ceiling: run the same area in price bands.

## `minBeds` (type: `integer`):

0 disables.

## `minBaths` (type: `integer`):

0 disables.

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

Interior floor area. 0 disables.

## `minYearBuilt` (type: `integer`):

Four-digit year. Useful for skipping pre-1978 stock when lead-paint rules matter, or for isolating recent construction. 0 disables.

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

Locations processed in parallel. Redfin sheds load under bursts by returning empty responses, so this is deliberately conservative. Lower it if you see throttling.

## `requestTimeoutSecs` (type: `integer`):

Per-request timeout.

## `maxRetries` (type: `integer`):

Retries for connection resets and 5xx responses. Empty throttled responses are retried separately with a backoff.

## `flattenOutput` (type: `boolean`):

Emit flat dot-notation columns. The property records are already flat, so this mainly affects the summary rows.

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

Recommended for runs over a handful of locations. Redfin throttles by address, and a proxy spreads the requests.

## Actor input object example

```json
{
  "locations": [
    "https://www.redfin.com/city/30818/TX/Austin"
  ],
  "sourceDatasetField": "locationUrl",
  "listingType": "for-sale",
  "soldWithinDays": 90,
  "maxListingsPerLocation": 350,
  "includeLocationSummary": true,
  "minPrice": 0,
  "maxPrice": 0,
  "minBeds": 0,
  "minBaths": 0,
  "minSqft": 0,
  "minYearBuilt": 0,
  "maxConcurrency": 5,
  "requestTimeoutSecs": 60,
  "maxRetries": 2,
  "flattenOutput": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

Property records carry recordType 'listing'; rollups carry 'location-summary'.

# 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 = {
    "locations": [
        "https://www.redfin.com/city/30818/TX/Austin"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("eiv/real-estate-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 = { "locations": ["https://www.redfin.com/city/30818/TX/Austin"] }

# Run the Actor and wait for it to finish
run = client.actor("eiv/real-estate-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 '{
  "locations": [
    "https://www.redfin.com/city/30818/TX/Austin"
  ]
}' |
apify call eiv/real-estate-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,eiv/real-estate-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/kPCSko11b9umVTjFg/builds/1rCceQjZHbnwi7yC8/openapi.json
