# Realtor.com (`lukass/realtor-com`) Actor

Scrape real-estate listings from Realtor.com without the official API. Search by location or paste listing URLs and get price, address, beds, baths, size, lot, property type, year built, agent and broker info, photos, and listing URL as structured JSON, CSV or Excel.

- **URL**: https://apify.com/lukass/realtor-com.md
- **Developed by:** [Lukáš Širhal](https://apify.com/lukass) (community)
- **Categories:**
- **Stats:** 2 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.10 / 1,000 items from searches

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?

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

## 🏠 Realtor.com Scraper — For Sale, For Rent & Sold

Scrape property listings from [Realtor.com](https://www.realtor.com) by **location** (city, ZIP, county, state), by **search URL**, or by **property ID** — price, address, beds, baths, lot & building size, year built, HOA, agent, broker, MLS, schools, tax history, flood risk, price estimate and photos.

No Realtor.com API key. No browser. The actor talks to the same private GraphQL API the Realtor.com iOS/Android app uses (`api.frontdoor.realtor.com`) — a guest token is minted automatically, so there are **no credentials to supply**.

### ✨ Why this Realtor.com scraper

- **Four ways in** — a location string, many locations, Realtor.com URLs, or bare property IDs.
- **Real filters** — price, beds, baths, living area, lot size, property type, listing type, and sort — applied server-side, the same fields the app sends.
- **Deep detail** — optional per-property fetch adds year built, HOA, assigned schools with ratings, full tax history, price estimate, and flood/noise scores.
- **Incremental runs** — `maxUpdatedHoursAgo` keeps only listings touched in the last N hours, for a scraper scheduled several times a day.
- **Fan-out friendly** — point a run at a slice of a ZIP-code dataset (`zipCodesDatasetId` + offset/limit) and have every slice write into one `targetDatasetId`.
- **Resumable** — counters and the seen-ID set survive a migration, so a restarted run neither duplicates results nor overshoots `maxItems`.

### 🚪 What it can do

| Input | What happens |
|---|---|
| `location` / `locations` | Each is searched through Realtor.com's own API and paged through in full. |
| `postalCodes` | ZIP codes searched the same way (alias of `locations`). |
| `startUrls` — search page | The URL is turned back into a location search, filters applied. |
| `startUrls` — property page | The `M…` ID in the URL is fetched directly. |
| `propertyIds` | Fetched directly, no search at all. |
| `zipCodesDatasetId` | ZIP codes read from a dataset, sliced by offset/limit. |

### ⬇️ Input

| Field | Type | Default | Meaning |
|---|---|---|---|
| `location` | string | — | One location: city, ZIP, county, or state |
| `locations` | array | — | Several locations |
| `postalCodes` | array | — | ZIP codes (alias of `locations`) |
| `startUrls` | array | — | Realtor.com search or property URLs |
| `propertyIds` | array | — | Property IDs, e.g. `M1234-56789` |
| `zipCodesDatasetId` + `zipCodesOffset` / `zipCodesLimit` | — | — | ZIP-dataset fan-out slice |
| `searchType` | string | `for_sale` | `for_sale`, `for_rent`, `sold` |
| `priceMin` / `priceMax` | integer | — | Price range (monthly rent for rentals) |
| `bedsMin` / `bedsMax` | integer | — | Bedroom range |
| `bathsMin` | integer | — | Minimum bathrooms |
| `sqftMin` / `sqftMax` | integer | — | Living-area range (sq ft) |
| `lotSqftMin` / `lotSqftMax` | integer | — | Lot-size range (sq ft) |
| `propertyTypes` | array | — | single\_family, condo, townhomes, multi\_family, land, farm, … |
| `sort` | string | `relevance` | relevance, newest, price\_low, price\_high, sqft, lot\_size, sold\_date |
| `maxUpdatedHoursAgo` | integer | — | Keep only listings updated within this window |
| `fetchDetails` | boolean | `true` | Fetch each property's full record |
| `maxItems` | integer | — | Cap on properties scraped — and on run cost |
| `maxConcurrency` | integer | `10` | Parallel requests |
| `targetDatasetId` | string | — | Push into an existing dataset instead of the run's own |
| `schedulerTaskId` | string | — | Task to start when the crawl finishes |
| `proxy` | object | Apify Proxy | US residential recommended for large runs |
| `debugLog` | boolean | `false` | Log every request and page |

### 🧪 Example inputs

**A city, full detail:**

```json
{
    "location": "Austin, TX",
    "searchType": "for_sale",
    "priceMin": 500000,
    "propertyTypes": ["single_family"],
    "maxItems": 200
}
```

**Incremental sweep, scheduled every 8 hours:**

```json
{
    "zipCodesDatasetId": "YOUR_ZIP_DATASET_ID",
    "zipCodesOffset": 0,
    "zipCodesLimit": 1000,
    "maxUpdatedHoursAgo": 8,
    "targetDatasetId": "YOUR_TARGET_DATASET_ID"
}
```

**A search URL and two known properties:**

```json
{
    "startUrls": [
        { "url": "https://www.realtor.com/realestateandhomes-search/Brooklyn_NY" }
    ],
    "propertyIds": ["M1234-56789"],
    "maxItems": 50
}
```

**Fast, cheap sweep — no per-property requests:**

```json
{
    "location": "10001",
    "fetchDetails": false
}
```

### ⬆️ Output

One item per property. With `fetchDetails: false` the search fields are produced and the detail-only fields (year built, HOA, schools, tax history, estimate, flood/noise, features) come back `null`.

```json
{
    "propertyId": "8479764426",
    "listingId": "3000370836",
    "url": "https://www.realtor.com/realestateandhomes-detail/1600-Barton-Springs-Rd-Unit-5406_Austin_TX_78704_M84797-64426",
    "status": "for_sale",
    "propertyType": "condos",
    "listPrice": 950000,
    "listDate": "2026-09-03T12:56:42.000Z",
    "beds": 2,
    "baths": 3,
    "sqft": 1590,
    "lotSqft": 1028,
    "yearBuilt": 2008,
    "stories": 1,
    "garage": 2,
    "line": "1600 Barton Springs Rd Unit 5406",
    "city": "Austin",
    "stateCode": "TX",
    "state": "Texas",
    "postalCode": "78704",
    "county": "Travis",
    "latitude": 30.262351,
    "longitude": -97.761535,
    "hoaFee": 1230,
    "mlsId": "4548505",
    "mlsName": "UnlockMLS",
    "daysOnMarket": 1,
    "agents": [{ "name": "Roxanne Kahn Peasley", "email": "roxanne@austinrealestate.com", "phone": "5129948254" }],
    "broker": "Roxanne Kahn Peasley",
    "schools": [{ "name": "Austin High School", "rating": 7, "grades": ["9", "10", "11", "12"], "distance": 1.2 }],
    "taxHistory": [{ "year": 2024, "tax": 9000, "assessedTotal": 600000 }],
    "estimate": 970000,
    "floodFactor": 2,
    "noiseScore": 68,
    "mainPhoto": "https://ap.rdcpix.com/...jpg",
    "photos": ["https://ap.rdcpix.com/...jpg"],
    "detailScraped": true
}
```

### 💰 What does it cost?

**Pay per result. No monthly rental, no start fee** — you are billed only for properties written to the dataset.

| You get | Price | When |
|---|---|---|
| Items from search | **$1.50 / 1,000** | `fetchDetails: false` — the search record: price, address, beds, baths, size, status, agent, broker, MLS, photos |
| With details | **$3.00 / 1,000** | `fetchDetails: true` (default) — adds year built, HOA, assigned schools with ratings, tax history, price estimate, flood & noise scores |

Platform usage (compute, proxy, storage) is **included** — no separate usage bill on top.

**Discounts on higher Apify plans** — the price per property drops with your subscription:

| Your plan | Items from search | With details |
|---|---|---|
| Free | $1.50 / 1K | $3.00 / 1K |
| Starter | $1.45 / 1K | $2.80 / 1K |
| Scale | $1.30 / 1K | $2.50 / 1K |
| Business | $1.10 / 1K | $2.00 / 1K |

Set `maxItems` to cap what a run can cost, or set a maximum run cost in the Apify Console. Blocked or empty pages are never charged — the charge happens when the item is pushed.

### 🧰 Troubleshooting

**Fewer results than expected.** Check `maxUpdatedHoursAgo` — with it set, everything older than the window is skipped on purpose. Turn on `debugLog` to see per-page counts. A very large metro is paged 200 at a time; let it finish.

**A run returns nothing.** Realtor.com may be rate-limiting the run's IP. Use US residential proxies for large or repeated runs.

### 🔐 Authentication and proxies

The API needs no account. A guest bearer token is minted per run with the app's device grant — no captured credentials of any kind. Sustained traffic from one datacenter IP still gets throttled, so US residential proxies are recommended for large runs.

### 📝 Changelog

#### 2.0.0

- Rewired to Realtor.com's current GraphQL API (`api.frontdoor.realtor.com`) after the old mobile endpoint was retired. Adds location/ZIP/county/state and URL/ID inputs, server-side price/beds/baths/sqft/type filters, sorting, and deep per-property detail (schools, tax history, estimate, flood/noise).

#### 1.0.0

- First release. Merged three earlier Realtor.com actors into one.

# Actor input Schema

## `location` (type: `string`):

A single location to scrape, in plain text - a city (`Austin, TX`), ZIP code (`78701`), county (`Travis County, TX`) or state (`Texas`). Resolved by Realtor.com's own search. Leave empty if you use **Locations**, **Realtor.com URLs** or **Property IDs** instead.

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

Several locations to scrape in one run - each a city, ZIP, county or state. Every entry is searched with the same filters and listing type.

## `postalCodes` (type: `array`):

US ZIP codes to scrape. An alias for **Locations** kept for the incremental ZIP-sweep workflow; each ZIP is searched the same way.

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

Realtor.com search or property URLs. A **search URL** (`/realestateandhomes-search/Austin_TX`) is turned back into a location search. A **property URL** (`/realestateandhomes-detail/…_M1234-56789`) is fetched directly by its ID. Filters below apply to search URLs too.

## `propertyIds` (type: `array`):

Realtor.com property IDs to fetch directly (e.g. `M1234-56789` or the numeric `property_id`). These skip the search entirely.

## `zipCodesDatasetId` (type: `string`):

Dataset whose items hold ZIP codes (a `zip`, `postalCode` or `postal_code` field, or plain strings). For a scheduler that splits one big ZIP list across several runs - combine with the offset and limit below. Read-only.

## `zipCodesOffset` (type: `integer`):

Index of the first ZIP to take from the dataset above.

## `zipCodesLimit` (type: `integer`):

How many ZIPs to take from the dataset above. Empty means all of them from the offset onwards.

## `searchType` (type: `string`):

Which listings to scrape.

## `priceMin` (type: `integer`):

Lowest price to include. For **For rent** this is the monthly rent.

## `priceMax` (type: `integer`):

Highest price to include.

## `bedsMin` (type: `integer`):

Fewest bedrooms.

## `bedsMax` (type: `integer`):

Most bedrooms.

## `bathsMin` (type: `integer`):

Fewest bathrooms.

## `sqftMin` (type: `integer`):

Smallest interior size in square feet.

## `sqftMax` (type: `integer`):

Largest interior size in square feet.

## `lotSqftMin` (type: `integer`):

Smallest lot in square feet. 1 acre = 43,560 sq ft.

## `lotSqftMax` (type: `integer`):

Largest lot in square feet.

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

Property types to include. Empty means every type.

## `sort` (type: `string`):

Order the results are requested in.

## `maxUpdatedHoursAgo` (type: `integer`):

Keep only properties touched within this window - the incremental mode for a scraper scheduled several times a day. Empty scrapes everything regardless of age.

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

Request each property's own record (year built, HOA, schools, tax history, price estimate, flood/noise, features) instead of only what the search result carries. Richer output, one extra request per property. Billed as a with-details property ($3.00 / 1,000); turn off for a fast, cheap sweep ($1.50 / 1,000).

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

How many properties to scrape. Every returned property is billed, so this caps run cost. Empty scrapes everything the input covers.

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

How many requests run in parallel.

## `targetDatasetId` (type: `string`):

Push results into this dataset instead of the run's own. Lets several parallel runs of a split ZIP list write into one place.

## `schedulerTaskId` (type: `string`):

Task to start once the crawl finishes - the scheduler that checks whether the other slices of the same fan-out are done. Empty starts nothing.

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

Realtor.com's mobile API needs no credentials, but sustained traffic from one datacenter IP eventually gets blocked. US residential proxies are recommended for large runs.

## `debugLog` (type: `boolean`):

Log every request and page. Turn on when a run returns fewer results than expected.

## Actor input object example

```json
{
  "location": "Austin, TX",
  "locations": [
    "Austin, TX",
    "78704",
    "Brooklyn, NY"
  ],
  "postalCodes": [
    "90210",
    "10001"
  ],
  "startUrls": [],
  "zipCodesOffset": 0,
  "zipCodesLimit": 1000,
  "searchType": "for_sale",
  "priceMin": 500000,
  "priceMax": 1500000,
  "bedsMin": 3,
  "bathsMin": 2,
  "sqftMin": 1000,
  "sort": "relevance",
  "maxUpdatedHoursAgo": 8,
  "fetchDetails": true,
  "maxItems": 200,
  "maxConcurrency": 10,
  "proxy": {
    "useApifyProxy": true
  },
  "debugLog": false
}
```

# Actor output Schema

## `overview` (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 = {
    "location": "Austin, TX",
    "startUrls": [],
    "maxItems": 200,
    "proxy": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("lukass/realtor-com").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 = {
    "location": "Austin, TX",
    "startUrls": [],
    "maxItems": 200,
    "proxy": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("lukass/realtor-com").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 '{
  "location": "Austin, TX",
  "startUrls": [],
  "maxItems": 200,
  "proxy": {
    "useApifyProxy": true
  }
}' |
apify call lukass/realtor-com --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,lukass/realtor-com"
        }
    }
}

```

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/mh4xpioyd13tV9l2q/builds/zEmV6k1XyT1h7pKHR/openapi.json
