How to Scrape Verified B2B Email Leads from Google Maps (2026)
Most B2B lead lists are garbage — bought from brokers, months old, and riddled with generic info@ addresses that go nowhere. Google Maps is different. It's where businesses actively maintain their contact details because they want customers to find them.
This guide walks through pulling verified B2B email leads from Google Maps: scraping business data, validating emails via MX/SMTP checks, and exporting an outreach-ready CSV.
What We're Building
By the end you'll have a pipeline that:
- Searches Google Maps for a target category + location
- Pulls name, address, phone, website, rating
- Extracts email addresses from business websites
- Validates each email (MX record exists + SMTP reachable)
- Outputs a clean CSV ready for your outreach tool
Step 1: Scrape Google Maps Business Listings
Google Maps is a JavaScript-heavy app — simple requests + BeautifulSoup won't work. You need a Playwright-based scraper that renders the feed and navigates place pages.
Option A: Run it yourself (Python + Playwright)
pip install playwright crawlee
playwright install chromium
import asyncio
from playwright.async_api import async_playwright
async def scrape_maps(query: str, max_results: int = 50):
async with async_playwright() as pw:
browser = await pw.chromium.launch(headless=True)
page = await browser.new_page()
url = f"https://www.google.com/maps/search/{query.replace(' ', '+')}"
await page.goto(url, wait_until="networkidle")
# Dismiss cookie consent if present
try:
await page.click('button:has-text("Accept all")', timeout=3000)
except Exception:
pass
results = []
feed = page.locator('[role="feed"]')
while len(results) < max_results:
items = await page.locator('[role="feed"] > div[jsaction]').all()
for item in items:
name_el = item.locator('div.qBF1Pd')
name = await name_el.inner_text() if await name_el.count() else ""
if name and name not in [r["name"] for r in results]:
results.append({"name": name})
if len(results) >= max_results:
break
# Scroll to load more
await feed.evaluate("el => el.scrollTop += 1000")
await page.wait_for_timeout(800)
await browser.close()
return results
asyncio.run(scrape_maps("plumbers Chicago", max_results=30))
This approach works for small batches but hits rate limits fast and needs proxy rotation for anything serious.
Option B: Use a managed scraper
For production volumes or when you don't want to manage headless browsers, use the Google Maps Business Scraper on Apify. It handles consent dialogs, scroll pagination, proxy rotation, and extracts phone + website per listing. Pay-per-result pricing means no wasted compute on failed runs.
Step 2: Extract Emails from Business Websites
A Maps listing gives you a website URL. The actual email is usually on the /contact page, in the footer, or the <head> as structured data.
import httpx
from bs4 import BeautifulSoup
import re
EMAIL_PATTERN = re.compile(r"[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}")
def extract_emails_from_url(url: str) -> list[str]:
if not url:
return []
try:
resp = httpx.get(url, timeout=10, follow_redirects=True,
headers={"User-Agent": "Mozilla/5.0"})
resp.raise_for_status()
except Exception:
return []
soup = BeautifulSoup(resp.text, "html.parser")
# Remove script/style noise
for tag in soup(["script", "style"]):
tag.decompose()
text = soup.get_text(separator=" ")
emails = list(set(EMAIL_PATTERN.findall(text)))
# Filter junk
blocked = {"example.com", "sentry.io", "w3.org", "schema.org"}
emails = [e for e in emails if not any(b in e for b in blocked)]
return emails[:5] # Return top 5 per page
For higher yield, also check /contact, /about, and mailto: links explicitly:
def find_contact_emails(base_url: str) -> list[str]:
found = []
paths = ["", "/contact", "/contact-us", "/about"]
for path in paths:
url = base_url.rstrip("/") + path
emails = extract_emails_from_url(url)
found.extend(emails)
if found:
break # Stop on first hit to avoid hammering
return list(set(found))
Step 3: Validate Emails (MX + SMTP)
Sending to unvalidated addresses tanks deliverability. Two levels of validation:
Level 1 — MX record check (fast, no SMTP needed)
import dns.resolver # pip install dnspython
def has_mx_record(email: str) -> bool:
domain = email.split("@")[-1]
try:
answers = dns.resolver.resolve(domain, "MX", lifetime=5)
return len(answers) > 0
except Exception:
return False
Level 2 — SMTP reachability check (slower but reliable)
import smtplib
import socket
def smtp_verify(email: str, from_addr: str = "verify@yourdomain.com") -> bool:
domain = email.split("@")[-1]
try:
mx = dns.resolver.resolve(domain, "MX", lifetime=5)
mx_host = sorted(mx, key=lambda r: r.preference)[0].exchange.to_text()
with smtplib.SMTP(mx_host, 25, timeout=10) as smtp:
smtp.helo("yourdomain.com")
smtp.mail(from_addr)
code, _ = smtp.rcpt(email)
return code == 250
except (smtplib.SMTPException, socket.error, Exception):
return False
Note: Some mail servers block SMTP probing (250 on all addresses). MX check alone catches the obvious invalids — typos, defunct domains, disposable services.
Step 4: Score Business Fit
Not all leads are equal. A 4.8-star plumber with 200+ reviews who actively maintains their Google profile is a different prospect from a 2.1-star shop with one review from 2019.
def agency_fit_score(business: dict) -> int:
"""Score 0-100 indicating how likely the business needs marketing help."""
score = 0
rating = float(business.get("rating", 0))
review_count = int(business.get("review_count", 0))
has_website = bool(business.get("website"))
has_email = bool(business.get("email"))
# Good rating = established business worth reaching
if rating >= 4.0:
score += 30
elif rating >= 3.0:
score += 15
# Review count signals activity
if review_count >= 50:
score += 25
elif review_count >= 10:
score += 15
# Has web presence = open to digital
if has_website:
score += 20
# Has findable email = reachable
if has_email:
score += 25
return min(score, 100)
Step 5: Assemble the CSV
import csv
from pathlib import Path
def export_leads(leads: list[dict], output_path: str = "leads.csv") -> None:
fieldnames = [
"name", "category", "address", "phone", "website",
"email", "rating", "review_count", "fit_score",
"email_verified_mx", "email_verified_smtp"
]
with open(output_path, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=fieldnames, extrasaction="ignore")
writer.writeheader()
writer.writerows(leads)
print(f"Exported {len(leads)} leads → {output_path}")
Run the full pipeline:
async def main():
query = "HVAC contractors Boston"
# 1. Scrape listings
raw = await scrape_maps(query, max_results=100)
enriched = []
for biz in raw:
# 2. Extract email
emails = find_contact_emails(biz.get("website", ""))
email = emails[0] if emails else ""
# 3. Validate
mx_ok = has_mx_record(email) if email else False
smtp_ok = smtp_verify(email) if mx_ok else False
# 4. Score
biz["email"] = email
biz["email_verified_mx"] = mx_ok
biz["email_verified_smtp"] = smtp_ok
biz["fit_score"] = agency_fit_score(biz)
enriched.append(biz)
# Filter to only verified leads
verified = [b for b in enriched if b["email_verified_mx"]]
print(f"{len(verified)}/{len(enriched)} leads have verified emails")
export_leads(verified)
asyncio.run(main())
What You Get
A typical run on a single city + category (say "roofing contractors Dallas") returns:
- 80-150 raw listings
- 40-70% have a findable website
- 20-40% yield an extractable email
- 60-80% of those pass MX verification
That's roughly 15-50 verified B2B leads per search query, ready for Apollo, Instantly, or a plain smtp cold outreach script.
Skip the Setup
If you'd rather not manage Playwright, proxies, and DNS libraries, the Google Maps Business Scraper on Apify handles the heavy lifting. Input a search query and location, get back structured JSON with business details including extracted emails. Pay per result — no subscription.
For a ready-made CSV of 500 verified local leads in your target niche, there's also a done-for-you option on Gumroad ($39, delivered within 24h).
The full open-source pipeline is on GitHub: google-maps-lead-scraper
Common Pitfalls
Rate limiting: Google Maps throttles repeated scraping from the same IP. Rotate proxies or use Apify's built-in residential proxy pool.
JavaScript-only email rendering: Some sites load email via JS after page load. You need Playwright (not just requests) to catch these. A page.wait_for_selector('a[href^="mailto:"]') before extracting helps.
Catch-all SMTP servers: Some mail servers return 250 for any address. This is hard to detect without sending a real message. MX verification + bounce rate monitoring in your sending tool is the practical floor.
GDPR/CAN-SPAM: In the EU, B2B outreach to business emails found publicly is generally permitted under legitimate interest, but always include an unsubscribe path and don't spam.
Top comments (0)