# Realtor.com Agent Scraper Recent Sold Listings (`simpleapi/realtor-com-agent-scraper`) Actor

Realtor.com Agent Scraper extracts agent profiles and recent sold listings, including agent names, brokerage details, locations, property addresses, sale prices, dates, and listing URLs. Ideal for agent research, lead generation, market analysis, and real estate intelligence.

- **URL**: https://apify.com/simpleapi/realtor-com-agent-scraper.md
- **Developed by:** [SimpleAPI](https://apify.com/simpleapi) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.99 / 1,000 results

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

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 Agent Scraper — Recent Sold Listings

Realtor.com Agent Scraper — Recent Sold Listings turns a list of ZIP codes into a directory of real estate agents, each with contact details, active-listing price bands, and preview rows for their recently-sold properties — all from data Realtor.com's own agent-search API already returns in one request. It's built for real estate lead-generation teams sourcing agent contacts by territory, brokerages researching competitor agent activity and price positioning by ZIP, and market researchers who want a per-agent view of active-listing volume alongside recent-sale activity. No Realtor.com login is required. Every section below documents an input, an output field, or exactly what the recent-sold preview data does and doesn't include.

### What is Realtor.com Agent Scraper — Recent Sold Listings?

This Actor searches Realtor.com's agent-search GraphQL API by ZIP code, fetches each matched agent's detailed branding profile, and — from data the search step already returned but a plain agent scraper would discard — appends a price-band summary, an active-listing snapshot, and child rows for recently-sold listing previews.

Key capabilities, read from the source:

- **Zero extra requests for the added fields.** `extract_listing_features()` reads `price_range_min`, `active_listing_count`, `last_listing_date`, and the recent-sold preview data straight out of the `SearchAgents` payload the base agent search already fetches — enabling `includeActiveStats`/`includeRecentSales` adds no new HTTP calls, only reshapes data already in hand.
- **Recently-sold listings as honest previews, not fabricated full records.** `build_recent_sold_children()` builds one child row per sold-listing preview with only what Realtor.com's preview payload actually carries — beds, baths, city, state, and a photo. The source explicitly notes the preview payload carries no price, address, or sale date, and none of those fields are invented to fill the gap.
- **A soft-block guard keyed on the expected response container.** A `SearchAgents` or `AgentBrandingProfile` response that returns HTTP 200 but is missing its expected data container (`search_agents`/`agent_branding`) is treated as `SoftBlockError` — evidence of a WAF shell response — and triggers proxy rotation, rather than being mistaken for "no more data."
- **Chrome-impersonated requests against an Akamai/PerimeterX-class target.** Requests use `curl_cffi` Chrome TLS impersonation rather than a plain HTTP client, specifically because realtor.com's anti-bot layer is fingerprint-sensitive.
- **Sticky residential proxy escalation.** The proxy ladder starts direct, escalates to datacenter then residential on a block, and — once residential is reached — stays there (`sticky_residential`) for the remainder of the run rather than testing lower tiers again.
- **Uncharged mirror of child rows.** Recent-sold listing child rows are pushed to the charged default dataset and separately mirrored to an uncharged per-run child dataset for convenient standalone export.

### What data can I extract with Realtor.com Agent Scraper — Recent Sold Listings?

Every field below is read directly from `extract_agent_details()`, `extract_listing_features()`, and `build_recent_sold_children()` in `src/main.py` — the dataset's default view surfaces all 29 as table columns.

#### Agent rows (`type: "agent"`)

| Field | Example Value | Notes |
| --- | --- | --- |
| `agent_name` | `"Jane Doe, agent"` | Includes a realtor-vs-non-realtor designation |
| `website` | `https://www.realtor.com/realestateagents/abc123` | The agent's Realtor.com profile page, constructed from their `fulfillment_id` — not necessarily their personal or brokerage website (see FAQ) |
| `office_phone` / `mobile_phones` | phone numbers | Parsed from the agent's phone list by type |
| `areas_serviced` | comma-separated area names | |
| `office_company_name` / `company_website` | brokerage/office identity | Falls back from office to broker when office data is absent |
| `listing_count` / `sold_count` | `12` / `8` | Active listings / annual sold count |
| `review_count` | `24` | |
| `agent_photo` | avatar URL | |
| `price_range_min` / `price_range_max` | `450000` / `1200000` | Annual combined price band; `null` when `includeActiveStats` is off |
| `active_listing_count` / `active_listing_min` / `active_listing_max` / `last_listing_date` | active-listing snapshot | `null` when `includeActiveStats` is off |
| `recent_sold_listings_count` / `more_sold_available` | `10` / `true` | How many sold previews were emitted for this agent, and whether more exist |

#### Recent-sold listing child rows (`type: "listing"`, `is_child: true`)

| Field | Example Value | Notes |
| --- | --- | --- |
| `listing_beds` / `listing_baths` | `3` / `2` | |
| `listing_city` / `listing_state` | `"Newark"` / `"NJ"` | |
| `listing_photo` | photo URL | |
| `agent_name` / `office_company_name` / `zip_code` | inherited from the parent agent row | Links the listing back to its agent |

No price, street address, or sale date is ever present on a listing child row — Realtor.com's own preview payload for recently-sold listings simply doesn't carry those fields.

### Why not build this yourself?

Realtor.com's public site is protected by an Akamai/PerimeterX-class anti-bot layer that fingerprints plain HTTP clients via TLS/JA3 signature, so a naive scraper using a standard HTTP library gets blocked at the network layer before it ever reaches the application logic — this is exactly why the Actor uses Chrome-impersonated requests rather than a plain client. On top of that, a block from this target often looks like a normal HTTP 200 response with an empty or missing data container rather than a clear error code, so a scraper that only checks the status code can silently treat a block as "no results" and quietly under-report. This Actor validates that the expected response container is actually present before accepting a page as successfully fetched, and separately, it reuses the same search-step payload for the price-band and recent-sold data rather than making a second, redundant request per agent.

### How to use data extracted from Realtor.com Agent Scraper — Recent Sold Listings?

#### Real estate lead generation

Search your target ZIP codes and filter by `sold_count`/`active_listing_count` to identify the most active agents in a territory, using `office_phone`/`mobile_phones` for direct outreach.

#### Brokerage competitive research

Compare `price_range_min`/`price_range_max` and `active_listing_count` across agents in a ZIP to understand price positioning and production volume by competitor before entering or expanding in a market.

#### Market activity tracking

Use `recent_sold_listings_count` alongside `listing_city`/`listing_state` across a batch of ZIPs to get a rough sense of recent transaction activity and geographic spread, without needing per-listing sale prices.

#### AI agents and lead-scoring pipelines

Because every listing child row inherits `agent_name` and `zip_code` from its parent, an agent can reconstruct the full agent-to-listings relationship directly from the flat dataset without a separate join step.

### 🔼 Input sample

| Parameter | Required | Type | Description | Example Value |
| --- | --- | --- | --- | --- |
| `zipCodes` | **Yes** | array | US ZIP codes to pull agents from. | `["90211", "33139"]` |
| `maxItemsPerZipcode` | No | integer (1–10000) | Agents to collect per ZIP. Default `10`. | `25` |
| `sortOrder` | No | string enum | `RELEVANT_AGENTS`, `RATINGS_REVIEWS`, `MOST_SALES`, `MOST_RECENT_ACTIVITY`, `TESTIMONIALS_RECOMMENDATIONS`. Default `"RELEVANT_AGENTS"`. | `"MOST_SALES"` |
| `includeActiveStats` | No | boolean | Add price band + active-listing summary. No extra requests. Default `true`. | `true` |
| `includeRecentSales` | No | boolean | Emit recent-sold preview child rows. Default `true`. | `true` |
| `maxListingsPerAgent` | No | integer (0–100) | Cap on sold-preview child rows per agent. `0` = unlimited. Default `10`. | `5` |
| `maxRetries` | No | integer (1–10) | Per-request retries before rotating the proxy IP. Default `3`. | `3` |
| `proxyConfiguration` | No | object | Starts direct; auto-escalates to datacenter then sticky residential on a block. | `{"useApifyProxy": false}` |

```json
{
  "zipCodes": ["90211", "33139"],
  "maxItemsPerZipcode": 25,
  "sortOrder": "MOST_SALES",
  "includeActiveStats": true,
  "includeRecentSales": true
}
```

**Common pitfall:** `website` on an agent row is always the constructed Realtor.com profile page URL, never a personal or brokerage domain — the underlying API response does carry a separate `branding.website` field, but this Actor's extraction doesn't surface it as `website`. Use `company_website` for the brokerage/office's own domain instead.

### 🔽 Output sample

Output is one JSON row per agent, plus one additional row per recent-sold listing preview, pushed to the run's default dataset and charged as one `row_result` event per row.

```json
{
  "zip_code": "90211",
  "agent_name": "Jane Doe, agent",
  "website": "https://www.realtor.com/realestateagents/abc123",
  "listing_count": 12,
  "sold_count": 8,
  "office_phone": "310-555-0100",
  "mobile_phones": "310-555-0199",
  "office_company_name": "Acme Realty",
  "review_count": 24,
  "type": "agent",
  "is_child": false,
  "price_range_min": 450000,
  "price_range_max": 1200000,
  "active_listing_count": 5,
  "recent_sold_listings_count": 10,
  "more_sold_available": true
}
```

A recent-sold listing child row:

```json
{
  "type": "listing",
  "is_child": true,
  "zip_code": "90211",
  "agent_name": "Jane Doe, agent",
  "listing_beds": 3,
  "listing_baths": 2,
  "listing_city": "Beverly Hills",
  "listing_state": "CA",
  "listing_photo": "https://ap.rdcpix.com/..."
}
```

### How do you filter and target specific agents and listings?

**Sort order is the primary agent-ranking lever.** `sortOrder: "MOST_SALES"` surfaces the highest-volume agents first for lead-gen prioritization; `RATINGS_REVIEWS` is better suited to a quality-focused referral search.

**Turn off what you don't need for a leaner run.** Both `includeActiveStats` and `includeRecentSales` cost nothing extra to enable (same underlying data already fetched), so there's little reason to disable them unless you specifically want a minimal output schema.

**Cap listing volume per agent, not per ZIP.** `maxListingsPerAgent` controls sold-preview rows per individual agent — a ZIP with many highly-active agents can still produce a large total row count even with a low per-agent cap, since the cap multiplies by however many agents `maxItemsPerZipcode` returns.

Three real examples:

```json
{ "zipCodes": ["90211"], "sortOrder": "MOST_SALES", "maxItemsPerZipcode": 20, "maxListingsPerAgent": 5 }
```

Top producers in one ZIP with a modest sold-listing sample each.

```json
{ "zipCodes": ["90211", "33139", "10001"], "includeRecentSales": false, "maxItemsPerZipcode": 50 }
```

Multi-ZIP agent directory without sold-listing rows — leaner, agent-only output.

```json
{ "zipCodes": ["33139"], "sortOrder": "RATINGS_REVIEWS", "includeActiveStats": true }
```

Quality-ranked agent search with price-band context for referral sourcing.

### ▶️ Want to try other scrapers?

| Scraper | What it extracts |
| --- | --- |
| Apartments.com Scraper — Floor Plan & Units | Rental listings with floor plan detail |
| Google Maps Scraper by Radius | Local business listings by geographic radius |
| LinkedIn Company URL Mass Profile Finder | Bulk company URL discovery with executive contacts |
| Instagram Outreach Lead Scorer | Profiles scored for outreach fit |

### How to extract Realtor.com agent data programmatically

This Actor runs as a standard Apify Actor call — one API call in, structured JSON dataset out, using your Apify API token.

#### Python example

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_API_TOKEN>")
run = client.actor("<YOUR_USERNAME>/realtor-com-agent-scraper-recent-sold-listings").call(run_input={
    "zipCodes": ["90211", "33139"],
    "sortOrder": "MOST_SALES",
    "maxItemsPerZipcode": 25,
})

for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    if item.get("type") == "agent":
        print(item["agent_name"], item["office_phone"], item["sold_count"])
```

#### Export to spreadsheets or CRM

Every dataset supports one-click CSV/Excel export from the Apify Console. Filter on `type`/`is_child` to separate agent rows from sold-listing preview rows before loading into a CRM's Contact and Property objects respectively.

### Is it legal to scrape Realtor.com agent data?

Scraping publicly visible Realtor.com agent listings is generally lawful, since this data is published for anyone to view without logging in — the underlying legal question was tested directly in *hiQ Labs v. LinkedIn* (9th Cir.), which held that scraping public, non-password-protected data does not violate the U.S. Computer Fraud and Abuse Act. Agent names, phone numbers, and photos are business-professional contact data typically used for legitimate business outreach, but check Realtor.com's own Terms of Service for commercial-scale usage, and consult legal counsel for high-volume commercial applications, particularly around outbound phone/SMS regulations (TCPA) if using phone numbers for calling or texting campaigns.

### ❓ FAQ

#### Why doesn't the website field show the agent's real personal website?

The `website` field is built from the agent's `fulfillment_id` as a Realtor.com profile page URL — the underlying API response does include a separate real `branding.website` field, but this Actor's field extraction doesn't surface it under `website`. Use `company_website` for the brokerage/office's own domain instead.

#### Why do sold-listing rows have no price or address?

Realtor.com's recently-sold preview payload — the same data this Actor reads to build those child rows — genuinely doesn't include price, street address, or sale date. Rather than fabricate those fields, the Actor emits only what's actually present: beds, baths, city, state, and a photo.

#### Does enabling includeActiveStats or includeRecentSales slow the run down?

No — both are read from the same `SearchAgents` response the base agent search already fetches for every agent; enabling them adds zero additional HTTP requests.

#### What does more\_sold\_available mean?

It's Realtor.com's own flag indicating there are more recently-sold listings for that agent beyond what the preview payload returned — it doesn't mean this Actor can fetch them; the preview list itself is capped by Realtor.com's response, further capped by your `maxListingsPerAgent`.

#### What happens if realtor.com blocks a request mid-run?

The Actor detects a WAF shell response (HTTP 200 with a missing expected data container) and rotates through a proxy ladder — direct, then datacenter, then sticky residential — retrying automatically rather than ending the run or silently returning empty data.

#### How does this compare to other Realtor.com agent scrapers?

As observed on the Apify Store on 2026-07-26, `sian.agency/realtor-agent-scraper` covers agent-level data but doesn't document combining active-listing price bands and recently-sold preview rows from a single request; `ayk_6789/realtorpulse-listing-scraper` focuses on listings rather than agents. This Actor's zero-extra-request enrichment approach is unique among these.

#### Does this work with AI agent frameworks?

Yes — call it as a standard HTTP endpoint via the Apify API from any agent framework capable of making an API call; there's no MCP-specific integration for this Actor.

### Conclusion

Realtor.com Agent Scraper — Recent Sold Listings turns a ZIP code list into an agent directory enriched with price-band context and recent-sold activity — all pulled from a single search request per page, with honest preview-only sold-listing data rather than fabricated pricing. It fits lead generation, competitive brokerage research, and market activity tracking. Start a run from the Apify Console or the Apify API with your target ZIP codes to get your first agent export.

# Actor input Schema

## `zipCodes` (type: `array`):

One or more US ZIP codes to pull agents from. Example: \["90211", "33139"]. Bulk input supported.

## `maxItemsPerZipcode` (type: `integer`):

Cap on how many agents to collect per ZIP code. Example: 10 returns the top 10 agents for the chosen sort. Default is 10.

## `sortOrder` (type: `string`):

Order in which agents are ranked within each ZIP.

## `includeActiveStats` (type: `boolean`):

Add each agent's annual price band (priceRangeMin/Max) plus active-listing count, list-price band, and last-listing date. Data comes from the same request — no extra cost. Default is true.

## `includeRecentSales` (type: `boolean`):

Emit one extra child row per recent-sold preview (beds, baths, city, state, photo) beneath each agent, and add a recentSoldListingsCount to the agent row. Default is true. Note: previews carry no sale price/address/date.

## `maxListingsPerAgent` (type: `integer`):

Cap the number of recent-sold child rows per agent. Example: 3 keeps up to 3 preview rows each. Use 0 for unlimited. Default is 10.

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

Per-request retry attempts before rotating the proxy IP. Default is 3.

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

Starts direct (no proxy). On a block or WAF shell it auto-falls back to datacenter, then sticky residential with retries.

## Actor input object example

```json
{
  "zipCodes": [
    "07101",
    "00501"
  ],
  "maxItemsPerZipcode": 10,
  "sortOrder": "RELEVANT_AGENTS",
  "includeActiveStats": true,
  "includeRecentSales": true,
  "maxListingsPerAgent": 10,
  "maxRetries": 3,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

All scraped items in the Actor's default dataset.

# 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 = {
    "zipCodes": [
        "07101",
        "00501"
    ],
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("simpleapi/realtor-com-agent-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 = {
    "zipCodes": [
        "07101",
        "00501",
    ],
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("simpleapi/realtor-com-agent-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 '{
  "zipCodes": [
    "07101",
    "00501"
  ],
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call simpleapi/realtor-com-agent-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,simpleapi/realtor-com-agent-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/seUAuhY6SdtjQ6QxJ/builds/wtchU2qZvGr1fLPTF/openapi.json
