The Mine Works
← All posts
use-case July 11, 2026 · 5 min read

Real Estate Agent Lead Lists: Extracting Contact Data from Redfin and Realtor.com

Build agent and brokerage lead lists by scraping Redfin and Realtor.com listings together — deduping agents across both sources for wider coverage.

Try the scraper

The actor referenced in this article. Pay only for results delivered.

View the scraper →

If you sell software, marketing services, or anything else to real estate agents, your first problem is not the pitch — it is the list. There is no clean, current directory of “every active listing agent in Austin, TX” that you can just buy. MLS access is gated to licensed members. Agent directories on Zillow, Redfin, and Realtor.com exist to sell leads to agents, not to hand out agent contact data to third parties.

But the listing pages themselves are public, and every for-sale listing on Redfin and Realtor.com carries the name of the agent and the brokerage representing it. Scrape enough listings in a market and you have a lead list — every active agent, their brokerage, and (indirectly, through the brokerage) a way to reach them.

This post covers building that list using the Redfin Scraper and Realtor.com Scraper together, and why running both sources beats running either alone.

Why one source isn’t enough

Redfin and Realtor.com pull from the same underlying MLS feeds in most markets, but their published inventory doesn’t match listing-for-listing. Sync delays, market-specific feed partnerships, and each site’s own filtering mean a meaningful slice of active listings shows up on one site and not the other at any given moment. If you only scrape Redfin, you’ll systematically miss agents whose listings sync faster to Realtor.com, and vice versa.

Running both and merging on agent name plus brokerage gets you closer to full market coverage than either source alone, and it also acts as a cross-check: an agent who shows up on both platforms with the same brokerage is a more reliable, currently-active data point than one you’ve only seen once.

What each actor returns

The Redfin Scraper takes a location (city, ZIP, or Redfin URL) and returns a flat JSON record per property, including listingAgentName and listingBrokerName, plus the address, price, beds, baths, MLS ID, days on market, and the listing URL. No login, no API key — it talks to Redfin’s own listing endpoints over HTTP and bills pay-per-result, $2 per 1,000 results, nothing charged on failure.

The Realtor.com Scraper works the same way against Realtor.com: give it a location, get back agentName and officeName per listing, along with price, beds, baths, sqft, county, and listing status. Same pricing model — $2 per 1,000, nothing charged on failure.

Note the field names differ between the two actors (listingAgentName/listingBrokerName on Redfin vs. agentName/officeName on Realtor.com) — normalize these when you merge datasets.

Building the merged lead list

import os
from apify_client import ApifyClient
from collections import defaultdict

apify = ApifyClient(os.environ["APIFY_TOKEN"])

LOCATION = "Austin, TX"

# Pull active listings from both sources
redfin_run = apify.actor("themineworks/redfin-scraper").call(run_input={
    "location": LOCATION,
    "maxItems": 1000,
})

realtor_run = apify.actor("themineworks/realtor-scraper").call(run_input={
    "location": LOCATION,
    "maxItems": 1000,
    "soldMode": False,
})

# Normalize both sources into a common shape
agents = defaultdict(lambda: {"listings": 0, "sources": set(), "brokerage": None})

for row in apify.dataset(redfin_run["defaultDatasetId"]).iterate_items():
    name = row.get("listingAgentName")
    if not name:
        continue
    key = name.strip().lower()
    agents[key]["listings"] += 1
    agents[key]["sources"].add("redfin")
    agents[key]["brokerage"] = row.get("listingBrokerName")
    agents[key]["display_name"] = name

for row in apify.dataset(realtor_run["defaultDatasetId"]).iterate_items():
    name = row.get("agentName")
    if not name:
        continue
    key = name.strip().lower()
    agents[key]["listings"] += 1
    agents[key]["sources"].add("realtor.com")
    if not agents[key]["brokerage"]:
        agents[key]["brokerage"] = row.get("officeName")
    agents[key]["display_name"] = name

# Agents confirmed on both platforms are your highest-confidence leads
confirmed = {k: v for k, v in agents.items() if len(v["sources"]) == 2}
single_source = {k: v for k, v in agents.items() if len(v["sources"]) == 1}

print(f"Total unique agents: {len(agents)}")
print(f"Confirmed on both platforms: {len(confirmed)}")
print(f"Single-source only: {len(single_source)}")

for key, data in sorted(confirmed.items(), key=lambda x: -x[1]["listings"])[:10]:
    print(f"{data['display_name']}{data['brokerage']}{data['listings']} listings")

Matching on lowercased full name is a reasonable first pass, but agent names collide (there is more than one “John Smith” in most metros). If you’re building a list for outreach at scale, add brokerage name as a second matching key, or run a fuzzy-match pass and manually review anyone with an ambiguous match before adding them to a CRM.

What you get and what you don’t

Both actors return the agent’s name and brokerage — they do not return an email address or phone number, because listing pages don’t publish those directly. The realistic workflow is: scrape the agent + brokerage, then either look up the brokerage’s public contact page for a general line, or run the agent name through a separate enrichment step if you need a direct email. Treat the scraped data as identification, not a finished contact record.

Also worth flagging: agent names on public listing pages are personal data in most jurisdictions’ data protection frameworks (GDPR-adjacent rules apply even to B2B contexts in some regions). If you’re building this for outbound sales, make sure your outreach has a legitimate basis and complies with applicable anti-spam and data protection rules in the markets you’re targeting.

Prioritizing the list

Once you have a merged dataset, a few simple signals separate active, high-value agents from noise:

  • Listing volume. Agents with 5+ active listings in your target market are working full-time and are worth prioritizing over agents with a single listing.
  • Cross-platform confirmation. Agents appearing on both Redfin and Realtor.com with matching brokerage are more likely to be current, verified professionals rather than stale MLS records.
  • Brokerage size. Group by listingBrokerName/officeName to see which brokerages dominate a metro — useful if you’re selling at the brokerage level rather than the individual agent level.
  • Price band. If your product targets luxury or first-time-buyer segments specifically, filter listings by price before extracting agents, so your lead list matches the segment you actually sell to.

Re-run both scrapers on a schedule (weekly is reasonable for most metros) to catch new agents entering the market and drop agents whose listings have gone stale, keeping the list current rather than a one-time snapshot.

For non-real-estate lead generation

This agent/brokerage approach is specific to real estate, where public listing pages happen to carry agent identity as a byproduct of the listing data. If you’re building B2B lead lists outside real estate — for SaaS sales, agency prospecting, or general company/contact discovery — see our B2B Leads Finder guide for a scraping-based alternative to tools like Apollo.

Related Actor

Explore the scraper referenced in this article — see inputs, outputs, and pricing, then run it on Apify.