Skip to main content
Thirdwatchthirdwatch
Real estate

Build Property Management Lead Lists From Apartments.com

Turn advertised rental inventory into a contactable list of leasing offices and rental brokers, with property name, address, leasing phone and portfolio size.

Sep 8, 2026 · 7 min read · 1,586 words
See the scraper →

To build a property management lead list, run the US Rentals Scraper with source: "apartments" across your target metros' ZIP codes and keep listing_name, address, phone and floorplans. Apartments.com publishes the leasing-office phone on the search page, so a contactable list needs no detail-page pass. Group by phone to collapse a portfolio into one operator, and rank by managed inventory. Built for proptech, insurance, maintenance and lending teams selling into multifamily.

Why advertised inventory is a better prospect list than a licence roster

Anyone selling into rental housing — leasing software, renters insurance, smart-lock hardware, turn services, laundry, landscaping, short-term lending — faces the same list problem. A purchased database of property management companies tells you a company exists. It does not tell you whether it has vacancy right now, how many units it runs, or which metro its inventory sits in. Most of those rows are dead on arrival.

Advertised inventory inverts the problem. The US Census Bureau's County Business Patterns counts tens of thousands of establishments in real estate property management, but only a fraction are advertising units in your territory this month. A leasing office with eleven live listings is spending marketing money right now, staffing a leasing desk right now, and has a turn problem right now. That is a buying signal a static roster cannot produce.

The output you want is one row per operator, not one row per listing: name, phone, the ZIP codes they operate in, the number of distinct properties, the number of floor plans across them, the rent band, and the concessions they are running. That last field is a conversation opener — an operator advertising two months free has a vacancy problem and a receptive ear.

How does this compare to the alternatives?

Prospect lists for multifamily come from three places. A bought database is broad and stale. Manual browsing of listing sites is accurate and does not scale past a few dozen properties. A listings pull is both current and scalable, and it is the only one of the three that carries the operational context — vacancy, unit mix, concessions — that makes the first call land.

Approach Cost model Reliability Setup time Maintenance
Bought B2B contact database Per-record or annual licence Broad coverage, no vacancy or inventory signal Days, plus procurement Vendor refresh cadence
Manual browsing and copy-paste SDR hours Accurate, does not scale Ongoing forever Redo every quarter
DIY Python scraper Your servers and infrastructure Breaks on layout changes at either site 3-7 days You own every breakage
Thirdwatch US Rentals Scraper Transparent pay-per-result Maintained; leasing phone on search, no detail pass needed 15 minutes Thirdwatch tracks site changes

The US Rentals Scraper covers the rental side of this. If you also sell to sales-side listing agents rather than leasing offices, building agent lead lists from Zillow is the same idea applied to for-sale inventory.

How to build a property management lead list in 4 steps

How do I pick territories and pull the inventory?

Territory is ZIP codes, not cities, for the same reason it always is: both sites cap how deep a single search paginates, so one city query returns a slice. Pass each ZIP as its own entry in queries. Use source: "apartments" when you want leasing offices, and leave includeDetails off — the phone number is already on the search card.

import os, requests, pandas as pd

ACTOR = "thirdwatch~us-rentals-scraper"
TOKEN = os.environ["APIFY_TOKEN"]

TERRITORY = ["30303", "30305", "30306", "30307", "30308",
             "30309", "30310", "30312", "30313", "30318"]

resp = requests.post(
    f"https://api.apify.com/v2/acts/{ACTOR}/run-sync-get-dataset-items",
    params={"token": TOKEN},
    json={
        "queries": TERRITORY,
        "source": "apartments",
        "maxResults": 200,
        "includeDetails": False,
        "propertyTypes": ["apartments"],
        "proxyConfiguration": {"useApifyProxy": True, "apifyProxyCountry": "US"},
    },
    timeout=3600,
)
props = pd.DataFrame(resp.json())
print(f"{len(props)} properties, {props['phone'].notna().sum()} with a leasing phone")

Restricting propertyTypes to apartments keeps the list to managed communities. Drop the filter if you also want small operators running scattered houses, condos and townhomes.

How do I collapse listings into operators?

One phone number usually fronts a whole portfolio. Group on it, and each group becomes a prospect with a measurable size rather than a single property.

props["ask_low"] = props["price"].fillna(props["price_min"])
props["plan_count"] = props["floorplans"].apply(
    lambda p: len(p) if isinstance(p, list) else 0
)

operators = (
    props.dropna(subset=["phone"])
    .groupby("phone")
    .agg(
        properties=("listing_id", "nunique"),
        plans=("plan_count", "sum"),
        zips=("zip_code", lambda s: sorted(set(s.dropna()))),
        example=("listing_name", "first"),
        median_ask=("ask_low", "median"),
        with_specials=("specials", lambda s: s.notna().sum()),
    )
    .reset_index()
    .sort_values(["properties", "plans"], ascending=False)
)
print(operators.head(15))

Two prospects fall out of this immediately. A phone with eight or more distinct listing_id values is a portfolio operator worth an account-based approach. A phone appearing once, on a property with many floorplans, is a single large asset — a different pitch, usually to an on-site manager rather than a head office.

How do I add rental agents and brokerages from the second source?

Realtor.com is the other half of the market and it names people rather than offices. Its rows carry agent_name and broker_name on the search card, but the phone only populates with includeDetails on — so this is where a capped detail pass earns its keep.

resp = requests.post(
    f"https://api.apify.com/v2/acts/{ACTOR}/run-sync-get-dataset-items",
    params={"token": TOKEN},
    json={
        "queries": TERRITORY,
        "source": "realtor",
        "maxResults": 100,
        "includeDetails": True,
        "maxDetailPages": 40,
    },
    timeout=3600,
)
agents_raw = pd.DataFrame(resp.json())

brokers = (
    agents_raw.dropna(subset=["broker_name"])
    .groupby("broker_name")
    .agg(
        listings=("listing_id", "nunique"),
        agents=("agent_name", lambda s: sorted(set(s.dropna()))),
        phone=("phone", lambda s: s.dropna().iloc[0] if s.notna().any() else None),
        median_ask=("price", "median"),
    )
    .reset_index()
    .sort_values("listings", ascending=False)
)

maxDetailPages is the cost control here: enrich the forty highest-value listings rather than every row. Rank the search results on price or listing count first, then enrich the top of that ranking.

How do I keep the list current and export it?

Re-run monthly on an Apify schedule and diff against last month's operator table. New phone values are new operators in your territory; operators whose listing count jumped are the ones who just took on inventory.

export = operators.assign(
    zips=operators["zips"].apply(", ".join),
    tier=lambda d: pd.cut(d["properties"], [0, 1, 3, 8, 10**6],
                          labels=["single", "small", "mid", "portfolio"]),
)
export.to_csv("atlanta_pm_operators.csv", index=False)

Keep listing_url on the underlying rows even though it does not appear in the operator roll-up. When a rep asks "where did this come from?", the answer needs to be a link, not a shrug.

Sample output

Two rows from a lead pull. The first is an Apartments.com community with the leasing phone on the search card; the second is a Realtor.com rental with a named agent and brokerage.

[
  {
    "listing_id": "s286zb6",
    "source": "apartments",
    "source_query": "78758",
    "listing_url": "https://www.apartments.com/the-watson-austin-tx/s286zb6/",
    "listing_name": "The Watson",
    "address": "11901 Burnet Rd, Austin, TX 78758",
    "city": "Austin", "state": "TX", "zip_code": "78758",
    "phone": "+1-832-730-2535",
    "agent_name": null,
    "broker_name": null,
    "price_min": 1615, "price_max": 4884,
    "beds_min": 0.0, "beds_max": 2.0,
    "property_type": "apartment",
    "floorplans": [
      {"name": "S1WF", "beds": 0, "baths": 1, "square_feet": 434, "price": 1615},
      {"name": "A2", "beds": 1, "baths": 1, "square_feet": 712, "price": 1889}
    ],
    "specials": "2 Months Free",
    "listing_status": "for_rent",
    "scraped_at": "2026-09-07T22:04:48+00:00"
  },
  {
    "listing_id": "4471203856",
    "source": "realtor",
    "source_query": "78704",
    "listing_name": "2107 Ford St",
    "address": "2107 Ford St, Austin, TX 78704",
    "zip_code": "78704",
    "phone": "+1-512-555-0184",
    "agent_name": "Dana Whitfield",
    "broker_name": "Austin Realty Group",
    "price": 2950,
    "beds": 2.0, "baths": 1.0, "square_feet": 1080,
    "property_type": "house",
    "days_on_market": 19,
    "listing_status": "for_rent",
    "scraped_at": "2026-09-07T22:05:02+00:00"
  }
]

The two rows are shaped differently on purpose. Apartments.com rows carry phone and listing_name but never agent_name, broker_name or days_on_market, because the site does not publish them. Realtor.com rows carry the human names on search and the phone once detail is on. specials is the softest, most useful field on the whole record for an opening line.

Common pitfalls

Treating one listing as one prospect. A portfolio operator appears dozens of times. Deduplicate on phone before the list goes anywhere near a dialler, or your reps will call the same office nine times in a week.

Expecting broker names on Apartments.com. They do not exist there. agent_name, broker_name, list_date and days_on_market are always empty on Apartments.com rows. Build the segmentation around source so no rep is ever handed a blank name field.

Paying for a detail pass you do not need. The Apartments.com leasing phone is already on the search card. Only the Realtor.com side needs includeDetails, and only on the shortlist — cap it with maxDetailPages.

Skipping the compliance screen. These are business contact details published on public listings, but US outreach rules apply independently. Check the FTC's Telemarketing Sales Rule guidance before a calling campaign and CAN-SPAM before an email one, and note that listing data used in housing advertising or tenant screening also falls under fair-housing rules.

Letting the list go stale. Vacancy is the signal, and vacancy expires. A quarterly refresh is the floor; monthly is better. Thirdwatch's Actor handles the anti-bot work and proxy rotation on both sites, so the refresh is a schedule rather than a project.

Related use cases

Frequently asked questions

Does Apartments.com publish a phone number on the search page?

Yes. The leasing-office phone comes back in the phone field on Apartments.com rows straight from search, so a contact list does not need the detail-page pass. Realtor.com is the opposite: it names the agent and brokerage on search but only returns a phone with includeDetails on.

Which source is better for B2B prospecting?

Apartments.com for property managers and multifamily operators, because it is weighted toward professionally managed communities with a leasing office. Realtor.com for rental agents and brokerages, because it carries agent_name and broker_name. Run source both and segment on the source field.

How do I tell a large operator from a single-property landlord?

Two signals. Communities with many entries in floorplans are larger assets, and a phone number or broker name that repeats across many listings identifies an operator running a portfolio. Group by phone and count distinct listing_id to rank prospects by managed inventory.

Is it legal to call numbers found this way?

The numbers are business contact details the sites publish on the listing itself, but US outreach is regulated separately. Screen against the FTC Telemarketing Sales Rule and Do Not Call rules before dialling and CAN-SPAM before emailing, and keep listing_url on every record as provenance.

Why are broker_name and agent_name empty on some rows?

Apartments.com does not publish a listing agent, broker or list date at all, so those fields stay empty on every Apartments.com row by design. Use phone and listing_name there. Agent and broker fields populate on Realtor.com rows where the listing names one.

Related

Try it yourself

100 free credits, no credit card.

About 30 real searches. Add the MCP to Claude or Cursor in two minutes.