# Realtor.com Agents Scraper (`scraper-engine/realtor-com-agents-scraper`) Actor

Realtor.com Agents by Zip Code Scraper collects detailed agent profiles from any U.S. ZIP code. It extracts names, offices, specialties, ratings, and contact data. Ideal for lead generation, market analysis, and building real estate agent databases.

- **URL**: https://apify.com/scraper-engine/realtor-com-agents-scraper.md
- **Developed by:** [Scraper Engine](https://apify.com/scraper-engine) (community)
- **Categories:** Lead generation, Real estate, Automation
- **Stats:** 24 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: 5.00 out of 5 stars

## Pricing

from $3.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 by ZIP — Phones, Brokerage, Sold Counts

Realtor.com Agent Scraper turns a list of US ZIP codes into structured JSON agent records: `agent_name`, `office_phone`, `mobile_phones`, `office_company_name`, `areas_serviced`, plus `listing_count`, `sold_count` and `review_count` for each agent. One row per agent, thirteen keys, no HTML parsing and no Realtor.com login. Open it on Apify, paste your ZIP codes and press **Start**.

⚠️ **`email` is always `null`.** Realtor.com does not publish agent email addresses on the public profile, so the Actor returns the field as `null` rather than guessing one. Phone numbers, the brokerage record and the real Realtor.com profile URL are returned instead. Details in [What data can I extract](#what-data-can-i-extract-with-realtorcom-agent-scraper).

### What is Realtor.com Agent Scraper?

Realtor.com Agent Scraper is an Apify Actor that reads Realtor.com's public real estate agent directory by ZIP code and returns one flat dataset row per agent. It queries the directory for each ZIP you supply, then opens each agent's public branding profile to pull contact numbers, brokerage details, service areas and production stats.

No Realtor.com account, login or cookie is required — every value it returns is already visible to any anonymous visitor on the agent's profile page. The only credential involved is your Apify token.

It is built for real estate lead generation teams building territory lists, brokerages running recruiting and competitive research, market analysts measuring agent density by ZIP, and developers piping agent records into a CRM or an AI pipeline.

### What Realtor.com agent data is publicly available to scrape?

Realtor.com publishes the whole agent profile — name, photo, brokerage, phone numbers, service areas, listing and sales counts, ratings — to anonymous visitors. What sits behind a gate is the messaging and account layer, not the profile data. One thing the public profile genuinely does not carry is an email address.

| Data Category | Public on the agent profile | Not published, or needs a Realtor.com account |
| ----- | ----- | ----- |
| Agent full name and realtor status | ✅ Returned as `agent_name` | — |
| Realtor.com profile URL and profile photo | ✅ Returned as `website` and `agent_photo` | — |
| Office and mobile phone numbers | ✅ Returned as `office_phone` and `mobile_phones` | — |
| Brokerage / office name and website | ✅ Returned as `office_company_name` and `company_website` | — |
| Cities the agent serves | ✅ Returned as `areas_serviced` | — |
| Active listings and homes sold in the last year | ✅ Returned as `listing_count` and `sold_count` | — |
| Number of client reviews | ✅ Returned as `review_count` | Individual review text — public, but **not returned** by this Actor |
| Languages, specializations, license / NRDS number, MLS history | ✅ Public on the profile — but **not returned** by this Actor | — |
| Agent email address | ❌ Not published on the public profile | Reachable only through Realtor.com's own contact form |
| Messages, saved agents, lead activity | ❌ | Realtor.com account |

Realtor.com Agent Scraper only returns publicly visible data — what any visitor sees on the agent profile. Nothing behind a login wall.

### What data can I extract with Realtor.com Agent Scraper?

Every agent row carries three groups of data: who the agent is and how to reach them, which business they work under, and how much they transact. Thirteen keys on a successful row, in the same order on every run.

| Field Name | Description |
| ----- | ----- |
| `agent_name` | Full name with the agent's Realtor.com status appended — `"Jane Doe, agent"` when the profile is flagged as a REALTOR®, `"Jane Doe, non-realtor"` when it is not. `null` when the profile publishes no name |
| `website` | The agent's real Realtor.com profile URL, taken from the profile's own `profile_url`. This is **not** the agent's personal site — see the note below the tables |
| `email` | **Always `null`.** Realtor.com does not publish agent email on the public profile, so no value is fabricated |
| `listing_count` | Number of active for-sale listings on the profile, or `null` when the profile publishes none |
| `sold_count` | Homes sold in the last year, as Realtor.com reports it, or `null` |
| `office_phone` | The office-type number on the brokerage record. Falls back to the agent's own office-type number when the brokerage record has none. `null` when neither exists |
| `mobile_phones` | The agent's mobile-type numbers, comma-joined into one string. `null` when the profile lists no mobile |
| `areas_serviced` | City names the agent serves, comma-joined into one string. `null` when the profile lists none |
| `office_company_name` | Brokerage or office company name. Falls back to the broker name when no office name is published |
| `company_website` | Office website, falling back to the broker website. `null` when neither is published |
| `review_count` | Number of client reviews on the profile, or `null` |
| `agent_photo` | URL of the agent's profile photo, or `null` |
| `zip_code` | The ZIP code from your input that this agent was found under — stamped onto every row |
| `error_reason` | **Present only on uncharged accounting rows.** Absent entirely from successful rows, so it is not a dead column in the table view. See [What happens when an agent profile cannot be read](#what-happens-when-an-agent-profile-cannot-be-read) |

#### Agent identity and contact fields

`agent_name`, `website`, `agent_photo`, `office_phone` and `mobile_phones` are the person half of the row. `agent_name` is a composite: the profile's full name, a comma, and either `agent` or `non-realtor` depending on the profile's REALTOR® flag — so split on the last comma if you need the bare name.

The two phone fields are read from two different places and mean different things. `mobile_phones` comes from the agent's own phone list, filtered to entries typed `mobile`, and joined with `", "` when the profile lists more than one. `office_phone` is read from the brokerage's phone list first, filtered to entries typed `office`; only if the brokerage publishes no office number does it fall back to an office-typed number on the agent's own record. That distinction matters for outreach — see the legal section.

> **Documented behaviour — `website` is a Realtor.com URL, not a personal one.** The field is populated from the profile's `profile_url`, which resolves to the live Realtor.com agent page. The underlying profile does expose a separate personal-website value, but this Actor deliberately does not surface it, so `website` means one thing on every row. Use `company_website` when you want an off-platform domain.

#### Brokerage and production fields

`office_company_name` and `company_website` describe the **business** the agent trades under, not the individual. Both cascade: office name before broker name, office website before broker website, `null` only when neither level publishes one. Paired with `office_phone`, these three keys are the brokerage-level slice of the row and can be separated from the personal fields cleanly — useful when you want a firm list rather than a person list.

`listing_count`, `sold_count` and `review_count` are the production signals. All three are integers copied straight from Realtor.com's own profile stats, and all three come back `null` rather than `0` when the profile publishes no figure — so an empty profile is never mistaken for a zero-sales agent. `areas_serviced` is the geographic footprint as a comma-joined city list, which is usually wider than the single ZIP that surfaced the agent.

#### 🤖 Add-on: Need additional real estate data?

The agent side pairs naturally with the inventory side. **Apartments.com Scraper** covers US rental buildings with rent bands, addresses and coordinates for the same geography, which is what you want when comparing an agent's territory against actual listing supply. **Google Maps Scraper with Reviews Sentiment Analysis** picks up the brokerage as a local business — opening hours, location and review sentiment — when `office_company_name` is the record you care about. For outreach lists beyond real estate, **LinkedIn Phone Lead Scorer** works the same contact-record shape on a different source.

### Why not build this yourself?

Realtor.com publishes no public agent API, so the alternative to this Actor is writing the access layer yourself — and on this site the access layer is the entire cost, not the parsing.

**Realtor.com fingerprints your TLS handshake.** The site sits behind an Akamai/PerimeterX-class anti-bot layer that JA3-fingerprints the client. As the Actor's source records, plain `aiohttp` or `requests` clients are soft-blocked *before* the request reaches the GraphQL gateway — you get an HTTP 200 carrying a data-less shell, or a dropped connection. This Actor uses `curl_cffi` with Chrome 124 TLS impersonation, which is what clears the check.

**Status codes cannot detect the block.** Because the anti-bot shell is served with HTTP 200, `raise_for_status()` waves it straight through and you silently record empty runs. The Actor instead keys detection on the *expected* GraphQL result container — `search_agents` for the directory query, `agent_branding` for the profile query — and treats a missing container, or a non-JSON body, as a `SoftBlockError` that triggers the proxy ladder. A loose keyword match on the body would be far less reliable.

**It is an N+1 request pattern against two undocumented GraphQL operations.** One `SearchAgents` call returns 24 agents; every one of those agents then needs its own `AgentBrandingProfile` call to get phones, brokerage, service areas and listing stats. Both operations require the internal `rdc-client-name` and `rdc-client-version` headers and a referer matching the directory page, and both carry hand-written query documents you have to keep in sync with the gateway.

On top of that you need a proxy escalation path with sticky residential sessions, pacing between pages and batches, retry budgets with exponential backoff, and per-agent error accounting so a blocked profile does not silently vanish from your list. That is infrastructure to run and pay for before you have written down a single phone number.

### How to use Realtor.com Agent Scraper

Realtor.com Agent Scraper runs on Apify. Start it from the Apify Console, or call it through the Apify API from your own code — there is nothing else to sign up for.

1. Open Realtor.com Agent Scraper on Apify and click **Try for free**
2. Fill in **Zip Codes** (`zipCodes`) — this is the one required input. Add as many US ZIP codes as you want; the field is prefilled with `07101` and `00501`
3. Set **Max Items Per Zip Code** (`maxItemsPerZipcode`) — the default is `10`, and the schema allows up to `10000`. This cap is **per ZIP**, not per run
4. Optionally change **Sort Order** (`sortOrder`) to pick which agents surface first, and leave **Proxy Configuration** alone unless you have a reason not to — the Actor manages its own escalation
5. Click **Start**, then export the dataset as JSON, CSV or Excel, or read it through the Apify API while the run is still going

**What a run with no ZIP codes does:** the Console blocks it, because `zipCodes` is marked required in the schema. An API caller who omits it or sends an empty list gets a run that logs `❌ No zip codes provided. Please provide at least one zip code.` and exits cleanly — no requests, no rows, nothing charged.

#### How to scale to bulk agent extraction

`zipCodes` is a list, so bulk extraction is the normal mode — paste a whole territory's worth of ZIP codes and each is processed in turn within one run, with the originating ZIP stamped onto every row as `zip_code`.

`maxItemsPerZipcode` applies **per ZIP code, not per run**: fifty ZIPs at `maxItemsPerZipcode: 200` is a 10,000-row target. There is no run-level row cap in the schema, so your total is the number of ZIPs multiplied by the per-ZIP cap — bounded in practice by how many agents Realtor.com actually lists for each ZIP.

ZIP codes are processed sequentially. Within a ZIP, the directory is paged 24 agents at a time and profile lookups run five agents concurrently.

### What can you do with Realtor.com agent data?

- 📞 A **brokerage recruiter** building a poaching list runs the ZIPs around a target office with `sortOrder: "MOST_SALES"`, then ranks rows by `sold_count` and `review_count` and reads `office_company_name` to see which firm each top producer currently sits under.
- 🗺️ A **real estate market analyst** measuring agent density runs a metro's full ZIP list at a high `maxItemsPerZipcode` and aggregates row counts and `listing_count` by `zip_code`, using `areas_serviced` to see which agents work well beyond the ZIP that surfaced them.
- 🏢 A **proptech founder** building a brokerage directory keeps only `office_company_name`, `company_website` and `office_phone`, deduplicates on the company name, and gets a firm-level table with no personal fields in it at all.
- 📇 A **RevOps engineer** enriching a real estate CRM matches incoming `agent_name` and `office_phone` against existing records, keeps `website` on the record as the provenance link, and routes `mobile_phones` through consent screening before it ever reaches a dialler.
- 🤖 An **AI engineer** building a market-research agent indexes `agent_name`, `office_company_name` and `areas_serviced` into a vector store with `zip_code`, `sold_count` and `listing_count` as metadata filters, so the agent can answer "which brokerages dominate these five ZIPs by sales volume" against live rows instead of a stale export.

Every one of these is callable from an agent framework over the Apify API, since the Actor is a standard HTTP-triggered run.

### How does Realtor.com Agent Scraper handle rate limits and blocking?

Realtor.com is fronted by an Akamai/PerimeterX-class anti-bot layer that fingerprints the TLS handshake, so the fetch path is built around clearing it rather than retrying into it.

Every request goes out through a `curl_cffi` session impersonating **Chrome 124**, with a 60-second timeout. The Actor **starts with no proxy at all** — the default `proxyConfiguration` prefill is `{"useApifyProxy": false}` — and escalates only when it has to. A blocked or unstable request moves it one rung up a ladder: direct → Apify **datacenter** proxy → Apify **residential** proxy. Residential is **sticky**: once the Actor lands there it stays there for the rest of the run, rotating to a fresh exit IP on each further block rather than dropping back down. If you select the `RESIDENTIAL` group yourself, sticky mode is on from the first request.

A request counts as blocked on HTTP 403, 429 or 503, on any transport-level error (timeout, connection reset, proxy error, DNS failure), on an error message containing `blocked`, `captcha` or `rate` — or on a `SoftBlockError`, which is what an HTTP 200 anti-bot shell raises when the expected GraphQL container is missing. Between attempts the Actor backs off exponentially (1s, 2s, 4s…). It also paces itself even when nothing is wrong: roughly 0.8–1.4 seconds between directory pages and 1.0–1.7 seconds between batches of five profile lookups.

There is **no CAPTCHA solving** in this Actor, and none is claimed.

#### What happens when an agent profile cannot be read

Nothing is silently dropped and nothing unreadable is billed. When an agent's profile lookup fails after its retries, the Actor still writes a row for that agent — carrying the identity it already knows from the directory payload — and pushes it **uncharged**, with a typed `error_reason`:

| `error_reason` | Meaning |
| ----- | ----- |
| `detail_fetch_failed` | The profile request was retried and never returned a usable response |
| `missing_profile_id` | The directory payload carried no profile id, so no profile lookup was possible |

Those rows keep `agent_name`, `sold_count`, `office_company_name`, `review_count` and `agent_photo` from the directory result, and set every field that only the profile can supply to `null`.

The Actor is on pay-per-event pricing and charges exactly one `row_result` event per successful, data-bearing agent row. **The marker is the presence of the key, not its value:** successful rows do not contain `error_reason` at all. Filtering is therefore `"error_reason" not in row` in Python, or a dataset filter on rows where `error_reason` is unset.

One failure mode is not soft: if the **first** directory page for a ZIP code fails after all retries and no agents have been collected yet, the Actor raises and the whole run stops there. ZIP codes after that one in the list are not processed. A page that fails *later* in the pagination is handled gracefully — the Actor logs it, keeps the agents already collected for that ZIP, and moves on to profile extraction.

### ⬇️ Input

Five parameters, read straight from the Actor's input schema. Only `zipCodes` is required.

| Parameter | Required | Type | Description | Example Value |
| ----- | ----- | ----- | ----- | ----- |
| `zipCodes` | **Yes** | array | ZIP codes to scrape agents from. Bulk input — each entry is processed in turn and stamped onto its rows as `zip_code`. Prefilled with `["07101", "00501"]`. | `["33139", "90210", "07101"]` |
| `maxItemsPerZipcode` | No | integer | Maximum agents to extract **per ZIP code**. Minimum `1`, maximum `10000`. Default `10`. | `50` |
| `sortOrder` | No | string | Order the directory returns agents in. Enum: `RELEVANT_AGENTS`, `RATINGS_REVIEWS`, `MOST_SALES`, `MOST_RECENT_ACTIVITY`, `TESTIMONIALS_RECOMMENDATIONS`. Default `RELEVANT_AGENTS`. | `"MOST_SALES"` |
| `proxyConfiguration` | No | object | Apify Proxy settings. The Actor starts direct and escalates to datacenter, then sticky residential, when Realtor.com blocks it. Prefilled `{"useApifyProxy": false}`. | `{"useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"]}` |
| `maxRetries` | No | integer | Retry budget for a blocked or unstable request before rotating the exit IP or giving up. Minimum `1`, maximum `10`. Default `3`. | `5` |

Four honest notes on how these behave:

- **`maxRetries` only governs the directory search, not the profile lookups.** The value you set is passed to the `SearchAgents` pagination requests. The per-agent `AgentBrandingProfile` requests use the code's own default of **3** attempts regardless of what you enter, as does the residential-proxy initialisation loop. Raising `maxRetries` to 10 therefore makes the directory phase more persistent and leaves the profile phase unchanged.
- **`proxyConfiguration` is read for `useApifyProxy` only.** If that flag is false — which is the prefilled default — the whole object is ignored and the run starts direct, so custom `proxyUrls` pasted in alongside `useApifyProxy: false` are **not used**. Set `useApifyProxy: true` for your configuration to be honoured as the starting mode. Once a block forces a fallback, the Actor switches to its own Apify datacenter or residential configuration.
- **`maxItemsPerZipcode` is a per-ZIP cap, and the Actor is not the only thing capping you.** The real ceiling for a ZIP is how many agents Realtor.com reports for it — the Actor stops paginating once it has collected that many, whatever your cap says.
- **ZIP codes are not validated.** There is no format check, length check or country check in the code; whatever strings you pass are sent verbatim to Realtor.com's directory query. A code Realtor.com does not recognise — a typo, a non-US postcode — simply comes back with an empty agent list, which produces no rows and no error row for that ZIP. The run continues to the next one.

Two things about the directory query are fixed in code and worth knowing: results are requested with **buyer-side agent intent**, and filtered to agents that have both an NRDS id and a fulfillment id on file. That is a deliberate quality filter, but it means the output is the licensed, fully-registered subset of a ZIP's agents rather than every name Realtor.com holds.

#### Example input

```json
{
  "zipCodes": ["33139", "90210", "07101"],
  "maxItemsPerZipcode": 50,
  "sortOrder": "MOST_SALES",
  "maxRetries": 3,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

### ⬆️ Output

Typed, normalized JSON with a consistent shape across runs — the same thirteen keys on every successful agent row, in the same order, regardless of how Realtor.com restyles its pages. Values a profile does not publish come back as `null`, never as `0` or `""`. Rows are pushed as each batch of agents finishes, so the dataset fills while the run is still going. Export as JSON, CSV or Excel, or read the dataset through the Apify API.

The dataset ships with a default table view that puts `zip_code` first and renders `website` and `company_website` as links and `agent_photo` as an image.

**Deduplication: there is none, by design and worth planning for.** An agent who serves several of the ZIP codes in your list is returned once per ZIP that surfaces them, each occurrence its own charged row with a different `zip_code`. Nothing is deduplicated within a ZIP either. If you are running dozens of adjacent ZIPs in one metro, expect overlap and collapse it yourself:

```python
seen, unique = set(), []
for row in items:
    key = row.get("website") or row.get("agent_name")
    if key and key not in seen:
        seen.add(key)
        unique.append(row)
```

#### Example output

A successful, charged agent row:

```json
{
  "agent_name": "Aleksandra Marzec, agent",
  "website": "https://www.realtor.com/realestateagents/567fe3af0fa4170100727005",
  "email": null,
  "listing_count": 27,
  "sold_count": 34,
  "office_phone": "(305) 760-9121",
  "mobile_phones": "(305) 401-8823, (786) 220-4417",
  "areas_serviced": "Miami Beach, Miami, Sunny Isles Beach, Surfside, Bal Harbour",
  "office_company_name": "The World Realty, Inc",
  "company_website": "https://www.theworldrealty.com/",
  "review_count": 265,
  "agent_photo": "https://ap.rdcpix.com/162885285/191adfd4aedfdf87891cf243b5bce3e3a-e0s.jpg",
  "zip_code": "33139"
}
```

An uncharged accounting row for an agent whose profile could not be read — the identity known from the directory is kept, profile-only fields are `null`, and `error_reason` is present:

```json
{
  "agent_name": "Marcus Whitfield, non-realtor",
  "website": null,
  "email": null,
  "listing_count": null,
  "sold_count": 6,
  "office_phone": null,
  "mobile_phones": null,
  "areas_serviced": null,
  "office_company_name": "Coastline Property Group",
  "company_website": null,
  "review_count": 0,
  "agent_photo": "https://ap.rdcpix.com/119043221/6c1f9d0f2b7e4a1c9d3e5f7a8b2c4d6ea-e0s.jpg",
  "error_reason": "detail_fetch_failed",
  "zip_code": "33139"
}
```

### How does it work?

Realtor.com serves its agent directory from an internal GraphQL gateway that refuses ordinary HTTP clients at the TLS layer, so the Actor speaks to it with a `curl_cffi` session impersonating Chrome 124 — the same handshake a real browser presents, without the memory cost of running one.

For each ZIP code it runs the directory's `SearchAgents` operation, 24 agents per page, following the offset until it has as many agents as your per-ZIP cap allows or Realtor.com runs out. It then fetches each agent's public branding profile through the `AgentBrandingProfile` operation, five at a time, and flattens the response into one row.

Success is never decided by HTTP status, because the anti-bot shell is served with HTTP 200. A response counts only once the GraphQL result container it should contain is actually present; anything else escalates the proxy ladder and retries.

Only publicly visible profile data is collected — no account, cookie or session is used anywhere. Because the thirteen output keys are defined by the Actor rather than by Realtor.com's markup, a site redesign can affect coverage but never your field names or types.

### Integrations

Realtor.com Agent Scraper is an Apify Actor, so it works with anything that can call the Apify API or consume a dataset — your own code, no-code automation platforms, and Apify's own schedules and webhooks.

#### Calling Realtor.com Agent Scraper programmatically

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")

run = client.actor("<YOUR_USERNAME>/realtor-com-agent-scraper").call(run_input={
    "zipCodes": ["33139", "90210"],
    "maxItemsPerZipcode": 50,
    "sortOrder": "MOST_SALES",
})

for row in client.dataset(run["defaultDatasetId"]).iterate_items():
    if "error_reason" not in row:
        print(row["zip_code"], row["agent_name"], row["office_phone"], row["sold_count"])
```

Works in Go, Ruby, Node.js, cURL — any language that can make an HTTP request. The `"error_reason" not in row` check is what separates charged agent rows from uncharged accounting rows.

#### Scheduling and webhooks

Attach an Apify **schedule** to re-run the same ZIP list monthly, which is the pattern for tracking agent movement between brokerages: each run writes its own dataset, and `website` (the stable profile URL) plus `office_company_name` give you a clean join key for diffing who changed firm. Add an Apify **webhook** on run success to POST the dataset id to your own endpoint so a completed run can trigger a CRM load without polling.

#### No-code tools (n8n, Make, LangChain)

In n8n, use the Apify node — or an HTTP Request node pointed at the Actor's run endpoint with your token — and pass the same JSON input shown above; an IF node checking whether `error_reason` exists splits charged rows from accounting rows. In Make, use the Apify module with run-and-wait and map the dataset straight into a Google Sheets, Airtable or HubSpot step. In LangChain, wrap the run call as a tool: rows are already flat typed JSON, so the tool output needs no parsing before it reaches the model.

### ⚠️ Is it legal to scrape Realtor.com agent profiles?

Scraping publicly accessible directory pages is broadly treated as permissible where no login is bypassed, and this Actor reads only what an anonymous visitor sees. But the output is **personal data about named professionals**, and it includes **personal contact data** — `mobile_phones` in particular. That changes your obligations rather than removing them.

A named individual with a phone number is personal data under every major regime. The moment you store a row, you are a data controller. Under GDPR and UK GDPR — which follow the controller, so they can apply to your processing wherever you operate, even when the agents themselves are in the US — you need a lawful basis before storing or reusing a row; relying on legitimate interest requires a documented balancing test, not an assumption, and transparency and erasure duties follow. Under CCPA/CPRA these individuals are squarely in scope, including the notice-at-collection and deletion-and-opt-out rights that attach to data you did not collect from them directly.

Using the data is regulated **separately, and more strictly, than collecting it**. Calling and texting in the United States is governed by its own federal and state rules, including do-not-call registries and wireless-specific restrictions; email falls under CAN-SPAM. "It was published on a directory" is not a lawful basis for calling, texting or emailing anyone.

Risk is also not uniform: `office_phone` is a brokerage business line, while `mobile_phones` is an individual's personal handset — materially different propositions. Screen against your suppression lists and DNC registries before any outreach, keep the brokerage fields separate from the personal ones where you can, and take advice from qualified counsel for your jurisdiction. This Actor gives you data; it does not give you permission to contact anyone.

### ❓ Frequently asked questions

#### What Realtor.com agent fields does Realtor.com Agent Scraper return?

Thirteen keys per successful row. The five you will use most are `agent_name`, `office_phone`, `mobile_phones`, `office_company_name` and `sold_count`, with `zip_code` stamped on every row so a multi-ZIP dataset stays sortable. See the [data fields table](#what-data-can-i-extract-with-realtorcom-agent-scraper) for all of them.

#### Does Realtor.com Agent Scraper require a Realtor.com account or login?

No. No account, no cookie, no session, no `KP_UIDz-ssn` value to copy out of your browser. The Actor reaches the public directory with Chrome TLS impersonation instead. The only credential involved is your Apify token.

#### Why is `email` always `null`?

Because Realtor.com does not publish agent email addresses on the public profile. The field is kept in the output with a `null` value rather than dropped or filled with a guess, so the column stays stable across runs and you can see at a glance that no address was available. Phone numbers, the brokerage record and the profile URL are what the public profile does carry.

#### How many agents can I extract in one run?

`maxItemsPerZipcode` accepts 1 to 10,000 and applies **per ZIP code**, so twenty ZIPs at 500 each is a 10,000-row target in one run. There is no run-level cap in the schema. Whether you reach the target depends on Realtor.com rather than on the Actor — the directory is paged 24 agents at a time and stops when the ZIP's own agent count is exhausted. Remember that agents serving several of your ZIPs produce one row per ZIP.

#### What happens if a ZIP code returns no agents, or is invalid?

You get no rows for that ZIP and the run carries on to the next one. There is no error row and no diagnostic row for an empty ZIP — the Actor logs `⚠️ No more agents found` and moves on. Nothing in the code validates ZIP format, so a typo or a non-US postcode is passed through to Realtor.com verbatim and comes back with an empty agent list, which looks identical to a genuinely agent-free ZIP. In code, treat a missing `zip_code` value in your results as "no match", not as a failure.

The one case that does stop a run: if the **first** directory page for a ZIP fails after all retries — a hard block rather than an empty result — the Actor raises and the remaining ZIP codes in the list are not processed. Individual agent profiles that fail behave differently and produce uncharged `error_reason` rows instead.

#### Can I scrape multiple ZIP codes at once?

Yes. `zipCodes` is a list and each entry is processed in turn within one run, with the originating ZIP written onto every row as `zip_code`. `maxItemsPerZipcode` then applies to each one independently. ZIPs run sequentially; within a ZIP, agent profiles are fetched five at a time.

#### Does Realtor.com Agent Scraper work with Claude, ChatGPT and other AI agent tools?

Yes. It is callable as a standard HTTP-triggered run through the Apify API, so LangChain, CrewAI, n8n or a hand-written tool definition can invoke it and receive typed JSON with no parsing step. The row shape is fixed at thirteen keys, so an agent tool schema for it is trivial to write.

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

Checked on the Apify Store on 25 July 2026, the three most visible alternatives take different angles.

`scraped/realtor-com-agents-by-zip-code` returns a near-identical field set — agent name, website, email, listing count, sold count, office phone, mobile phones, areas serviced, office name, company website, review count, photo — plus a `Zip codes serviced` column this Actor does not have. Two differences stand out on its listing: it asks you to paste a `KP_UIDz-ssn` cookie and User-Agent out of your browser's DevTools "if you are not getting results due to Realtor.com's site protection blocking", and its README states results for trial users are limited to 5 records. It is priced as a flat monthly rental. This Actor needs no cookie from you and handles the anti-bot layer in code.

`cleansyntax/realtor-com-agents-scraper` documents a much wider CSV — 29+ numbered base columns including first/last name split, email, city, state, languages, specializations, description, zips served, office address, MLS history and NRDS id — with state persistence for resumable runs and pagination it describes as 20 agents per page. If you need licence numbers, languages or specializations, that is the wider surface.

`memo23/realtor-search-cheerio` is a combined property-and-agent Actor whose listing leads on a pre-collected database of 200,000+ agents served without live scraping, and claims that avoids what it describes as Realtor.com's roughly 3,000-per-search cap. It also documents property listing scraping, which this Actor does not do at all.

Realtor.com Agent Scraper is narrower on fields by choice and explicit about the parts the others do not document: no cookie to supply, a soft-block detector keyed on the GraphQL result container rather than HTTP status, a direct→datacenter→sticky-residential proxy ladder, and per-agent uncharged error rows with a typed `error_reason`.

#### Does Realtor.com Agent Scraper return data in a format LLMs can use directly?

Yes. Typed, normalized JSON with the same field names across runs — flat keys, no HTML, no selectors, no stringified blobs. Pass a row straight into an LLM context window, index it into a vector store, or hand it to an agent tool. Guard on `"error_reason" not in row` if you want charged rows only.

#### What happens when Realtor.com changes its layout or anti-bot system?

The scraper is maintained, and it is deliberately insulated from layout change: rows are built from Realtor.com's own GraphQL responses rather than from CSS selectors, so a visual redesign does not move anything. Anti-bot changes are handled at the engine level — block detection keys on the expected result container and on transport errors, and profiles that stay unreadable degrade into typed, uncharged error rows rather than silently corrupting your dataset. Your thirteen field names and types stay put.

#### Can I use Realtor.com Agent Scraper without managing proxies?

Yes. The Actor pre-builds its own datacenter and residential Apify Proxy configurations at startup, starts direct, and escalates and rotates exit IPs by itself when Realtor.com pushes back. You never create a proxy account, pick a group or rotate an IP. It does not solve CAPTCHAs, and it makes no claim to.

#### Which Realtor.com agent fields work best for AI training data and RAG indexing?

For RAG indexing, `areas_serviced` and `office_company_name` carry the most descriptive text per record and chunk cleanly by market, with `agent_name` as the natural document title and `zip_code` as a ready-made metadata filter. For training data or feature extraction, `listing_count`, `sold_count` and `review_count` are the most structurally consistent fields, since all three are integers taken straight from Realtor.com's own stats and come back `null` rather than `0` when unpublished — so incomplete records can be dropped without misreading a zero as a real figure. Keep the personal-data obligations in the legal section in mind before pushing agent contact rows into a training set or a shared vector store.

### 🔗 Related scrapers

| Scraper Name | What it extracts |
| ----- | ----- |
| Apartments.com Scraper | US rental listings with rent bands, addresses, coordinates and transit |
| Booking Scraper | Accommodation listings with prices, ratings and location |
| Google Maps Scraper with Reviews Sentiment Analysis | Brokerages as local businesses, with coordinates and review sentiment |
| LinkedIn Phone Lead Scorer | Phone-based lead records and scoring on LinkedIn |
| LinkedIn Profile and Company Enrichment Scraper | Company and profile enrichment for the firms behind the agents |
| Instagram Phone Number Scraper | Publicly posted phone numbers indexed against instagram.com |

### 💬 Your feedback

Found a bug, or need a field that is on the Realtor.com agent profile but not in the output — languages, specializations, licence number, review text? Open an issue on the Actor's Issues tab and it will be looked at. Reports that include the exact input JSON and the ZIP code that misbehaved are the fastest to reproduce and fix.

# Actor input Schema

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

Enter zip codes to scrape agents from (e.g., \['07101', '00501']). Supports bulk input for multiple locations! 🎯

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

Maximum number of agents to extract per zip code. Set higher for more comprehensive results! ⬆️

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

Choose how to sort the agent results. Find the most relevant agents for your needs! 🎯

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

🛡️ Smart proxy management! Starts with no proxy by default. If Realtor.com blocks requests, automatically falls back to datacenter proxy, then residential proxy with 3 retries. Your scraping stays uninterrupted! ⚡

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

How many times to retry a blocked or unstable request before rotating the proxy exit IP or giving up. Increase for tougher network conditions.

## Actor input object example

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

# Actor output Schema

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

All scraped agent rows 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("scraper-engine/realtor-com-agents-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("scraper-engine/realtor-com-agents-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 scraper-engine/realtor-com-agents-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scraper-engine/realtor-com-agents-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/7KLy1oD4BIBOTDIKP/builds/SkiiahHMV5rzPrlZb/openapi.json
