Every business Google Maps lists for a search, with the email addresses, phone numbers and social accounts its own website publishes. One row per business, written to a CSV.
Google Maps shows a phone number and a website for most businesses and an email for none. These scripts call Google Maps Leads Scraper, which searches Maps and then reads each business's website (the home page and up to three pages it links to: contact, about, team, imprint) for the details Maps leaves out.
The write-up, with a full run on "dentists in miami fl", is How to get the email of every dentist in Miami from Google Maps.
You need an Apify account and its API token, from Console → Settings → API & Integrations.
Node (18 or newer):
cd node
npm install
APIFY_TOKEN=your_token node leads.mjs "dentists in miami fl"Python (3.11 or newer):
cd python
python -m venv .venv && . .venv/bin/activate
pip install -r requirements.txt
APIFY_TOKEN=your_token python leads.py "dentists in miami fl"Both write leads.csv with every business that published an email, and print
a summary. For the Miami run it reads:
182 businesses, 81 with an email -> leads.csv
found: 144
no_website: 20
unreadable: 16
none_found: 2
Put the place in the search, the way you'd type it into Maps: plumbers in denver co, wedding photographers in lisbon. A search without a place is
answered for wherever Google thinks the request came from.
sample-output.json holds four real rows from that run,
one for each case you will meet: two practices with contacts, one with no
website, and one whose website refused to be read. The fields you will use most:
| Field | What it holds |
|---|---|
name, rating, reviewCount, address, phone, website |
From the Maps listing |
email, emails |
Addresses found on the website, best first |
phoneE164, websitePhones |
The Maps number and the website's numbers, in international format |
instagram, facebook, linkedin, youtube, tiktok, twitter, whatsapp |
Accounts the website links to |
contactFormUrl |
The page with the contact form |
contactsStatus |
found, none_found, no_website, social_profile, unreadable or not_read |
contactsError |
Why a website was unreadable or not_read |
placeId |
Google's ID for the place; deduplicate across runs on it |
Filter on contactsStatus, not on whether email is empty: an empty email
can mean the site publishes none, the site refused to be read, or there is no
site at all.
$0.0005 per business, plus $0.0015 per business whose website gave contacts. The Miami run above was 182 × $0.0005 + 144 × $0.0015 = $0.31. A website that publishes nothing, cannot be read, or does not exist is not charged the second part.
- A site that refuses automated readers, or answers too slowly, comes back
unreadablewith the reason. On the Miami run that was 16 of 162 websites. - No browser runs, so a site that draws its page with JavaScript reads as empty.
- Google's list varies between runs of the same search. For full coverage of a
city, split the search by neighbourhood and deduplicate on
placeId. - Most searches stop at 120–140 businesses, where Maps itself stops scrolling.
- An address on a website is not consent to be emailed. Check the rules on unsolicited email where you and your recipients are.

