Google Maps is the biggest directory of local businesses there is, and if you sell to dentists, plumbers or restaurants it's where your lead list lives. The problem is that copying listings by hand is slow, and Maps never shows an email address. This guide shows how to scrape Google Maps from Python, get one clean row per business, and add the emails each business publishes on its own website.
Quick start
pip install apify-client
from apify_client import ApifyClient
client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("automly/google-maps-scraper").call(run_input={
"searchStringsArray": ["dentists in Chicago"],
"maxCrawledPlacesPerSearch": 50,
"includeContacts": True,
})
for place in client.dataset(run["defaultDatasetId"]).iterate_items():
print(place["title"], "|", place.get("phone"), "|", place.get("emails"))
That's a working Google Maps scraper in Python. Your token is in the Apify Console under Settings > API & Integrations. Not using Python? The same actor works as a plain Google Maps scraper API: a POST to Apify's run-sync-get-dataset-items endpoint with the same JSON input returns the rows as JSON.
Why not the official API?
Google does have a Google Maps API for this: the Places API (New). It works, but two things make it a poor fit for lead lists. It has no email field at all, and one Text Search query stops at 60 results across all pages. The phone, website, rating and opening hours fields also sit in the Enterprise tier, so asking for them bills the more expensive SKU.
Since March 1, 2025, Google no longer gives a $200 monthly credit. Each SKU now has its own free monthly cap instead. Prices below are the first volume tier from Google's pricing page, checked October 2026:
| Option | Access needed | Cost | What you get |
|---|---|---|---|
| Places API (New), Text Search Pro | Google Cloud project, billing account, API key | $32 per 1,000 requests, 5,000 free per month | Name, address, category, location. No phone, website or rating |
| Places API (New), Text Search Enterprise | Same | $35 per 1,000 requests, 1,000 free per month | Adds phone, website, rating, review count, hours. Up to 20 places per request, 60 per query. No email |
| Places API (New), Place Details Enterprise | Same | $20 per 1,000 requests, 1,000 free per month | The same Enterprise fields for one place per request. No email |
| Google Maps Scraper (this guide) | Free Apify account | $3.00 per 1,000 places, plus $0.01 per run and platform usage | Name, category, phone, website, address, rating, reviews, hours and more. Optional emails and social profiles from the business's website |
If you need a handful of lookups inside an app, the official API is fine. If you need every dentist in a city with an email column, you need something else.
Step 1: Pick what and where to search
There are two ways to say where. You can put the city in the query, like plumbers in Denver, or keep the query short and set locationQuery:
run_input = {
"searchStringsArray": ["plumber", "electrician"],
"locationQuery": "Austin, TX",
"maxCrawledPlacesPerSearch": 100,
"language": "en",
}
locationQuery takes a city, a state, a postal code or a country, and only places inside that area are kept. Each query in searchStringsArray runs separately, and maxCrawledPlacesPerSearch caps each one (the default is 100).
You can also skip searching and pass Google Maps links in startUrls or place IDs in placeIds if you already know which businesses you want.
Step 2: Turn on emails and social profiles
This is the part people mean when they search for a "Google Maps email scraper". Set includeContacts to True and the scraper visits each place's own website and adds three fields:
-
emails: addresses found on the site -
phones: phone numbers found on the site (often more than the one on Maps) -
socialProfiles: Facebook, Instagram, LinkedIn, X, YouTube, TikTok and Pinterest links
To be clear about where these come from: Google Maps itself doesn't publish emails. They come from the business's website. So places without a website get no email, and some websites simply don't list one. In a recent run of 15 Austin dentists, 14 came back with an email or a social profile.
Step 3: Filter before you pay for rows
Filters run before a place is saved, so skipped places aren't charged:
run_input.update({
"placeMinimumStars": "four", # also: "threeAndHalf", "fourAndHalf", ...
"placeMinimumReviews": 20,
"website": "withWebsite", # or "withoutWebsite", "allPlaces"
"skipClosedPlaces": True,
})
A tip if you sell web design: set website to "withoutWebsite". Those businesses are your leads, and you call them instead of emailing.
Step 4: Read the output
Here's a real row, trimmed and anonymized:
{
"title": "Example Hills Dentistry",
"categoryName": "Dentist",
"address": "Example Hills Dentistry, 1200 W Example Blvd #207, Austin, TX 78704",
"city": "Austin",
"postalCode": "78704",
"website": "https://www.example-dentist.com/",
"phone": "(512) 555-0147",
"phoneUnformatted": "+15125550147",
"totalScore": 5,
"reviewsCount": 354,
"openingHours": [{"day": "Friday", "hours": "8 AM-4 PM"}],
"placeId": "ChIJ...",
"url": "https://www.google.com/maps/place/?q=place_id:ChIJ...",
"emails": ["hello@example-dentist.com"],
"phones": ["+15125550147", "+17375550192"],
"socialProfiles": {
"facebook": ["https://www.facebook.com/example"],
"instagram": ["https://www.instagram.com/example"]
}
}
placeId stays the same between runs, which makes it a good key when you refresh a list and want to spot new businesses.
Step 5: Save to CSV
import csv
rows = client.dataset(run["defaultDatasetId"]).iterate_items()
fields = ["title", "categoryName", "phone", "website", "address",
"totalScore", "reviewsCount", "emails", "placeId"]
with open("leads.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=fields, extrasaction="ignore")
writer.writeheader()
for place in rows:
place["emails"] = "; ".join(place.get("emails") or [])
writer.writerow(place)
If you prefer pandas, pd.DataFrame(client.dataset(...).list_items().items) gets you there too.
What else can you scrape?
The same actor can add more to each place:
-
maxReviews: the newest (or most relevant) reviews per place, with text, rating, date and the owner's reply.reviewsStartDatelimits them to recent ones. -
maxImages: photo links with the date each was added. -
scrapePlaceDetailPage: popular times, review counts per star, review topics and "people also search for". -
categoryFilterWords: keep only places whose Google category matches, say,italian restaurant.
Scaling
One Google Maps search shows around 200 places, and a big city has far more. Set enableGridSearch to True and the area is split into smaller cells that are searched one by one and merged, so a single query can return thousands of places. gridCellKm sets the cell size: 5 for a quick pass, 2 for a thorough one, 1 for the most complete list. For a city that isn't recognized automatically, pass customBounds with south, west, north and east.
For a list you keep fresh, save the input as a task in Apify and put it on a weekly schedule.
No code? Use the Apify Console
Open the actor page, click Try for free, type your searches and location, flip Add emails and social profiles, and click Start. The results show in tables like Overview, Contacts and Website contacts, and download as CSV, Excel or JSON. Website contacts puts the name, website, emails, phones and social profiles side by side, and it's the one I'd hand to a sales team. Google Sheets, Zapier, Make and n8n can pick up each run.
What it costs
Right now places cost $3.00 per 1,000, plus $0.01 per run start and the run's Apify platform usage. Contact details have no separate charge today.
That changes on October 17, 2026, when contact details get their own price, charged only for places where the website gave at least one email, phone or social profile:
| Apify plan | Per 1,000 places with contacts found |
|---|---|
| Free | $1.80 |
| Starter | $1.60 |
| Scale | $1.20 |
| Business and up | $0.85 |
Every Apify account gets $5 of free usage per month, which covers roughly 1,500 plain places. The Pricing tab on the actor page always has the current numbers.
Responsible use
Stick to public business data. Some of it, like an owner's name or a personal email, can still be personal data under GDPR or CCPA, and cold outreach has its own rules such as CAN-SPAM. Follow Google's terms and ask a lawyer if you're unsure.
Links
If you want a lead list by this afternoon, try Google Maps Scraper on Apify. The free $5 a month is enough to test it on your own city.
Top comments (0)