# Google Maps Email Extractor (`steadyscrape/google-maps-email-extractor`) Actor

Google Maps email extractor: local business leads with emails and contact details from their own websites (phones, contact page, Facebook, Instagram, LinkedIn, X, YouTube, TikTok) plus Maps data: name, address, rating, reviews. Pay only per lead with an email. Export to JSON, CSV, Excel or API.

- **URL**: https://apify.com/steadyscrape/google-maps-email-extractor.md
- **Developed by:** [Steady Scrape](https://apify.com/steadyscrape) (community)
- **Categories:** Lead generation, Marketing
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 lead with an emails

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Google Maps Email Extractor — local business leads with emails

This **Google Maps email extractor** finds local businesses on Google Maps — dentists, plumbers, accountants, roofers,
any trade, in any city, area or postcode, within a radius you choose — and reads **each business's own website** for
its **email addresses**, phone numbers, contact page and social profiles (Facebook, Instagram, LinkedIn, X, YouTube,
TikTok). Every lead also carries the Google Maps data: name, categories, address, phone, rating, review count, opening
hours, whether the profile is claimed, and its Maps link.

**You only pay for leads with an email.** Businesses whose website shows none are skipped for free — or delivered at a
lower price if you ask for them.

### What the Google Maps email extractor is for

- **Agencies and freelancers selling to local businesses** (web design, SEO, ads, reviews, bookkeeping): a cold
  email list of every dentist in a city, with the owner's address on the practice's own domain first.
- **Lead resellers and SMMA teams**: many trades × many cities in one run, scheduled weekly with only the new leads.
- **B2B sales teams** building territory lists: email, phone, website and social profiles in one row, ready for a CRM.
- **Market research**: how many businesses of a kind there are around a point, and how many publish an email.

### How it works

1. You give **search terms** (`dentist`, `plumber`), **locations** (`Austin, TX`, `Leeds`, `78701`, or
   `latitude, longitude`) and a **radius**.
2. Each location is looked up on Google Maps. The Actor reads Google Maps' results around its centre, page after page,
   then around more points spread across the circle, so it reaches past the ~120–240 places a single Google Maps search
   shows. Places outside the radius are dropped.
3. For each place with **a website of its own** (a Facebook page or a Yelp listing in the website field is not one),
   the Actor reads that page like a visitor — then the **contact page** it links to (or the homepage, when Maps points
   to a deep page; or `/contact` when the menu is drawn by script). At most a handful of plain page requests per site.
4. Emails come from `mailto:` links, the page text (also `info [at] domain [dot] com`), Cloudflare's email protection
   and the page's structured data. **Junk is filtered out**: error-tracker ids, the site builder's or plugin's own
   addresses, template placeholders (`your@email.com`), image names (`logo@2x.png`), `noreply@` senders and domain-sale
   pages. Addresses on the business's own domain come first, then Gmail/Outlook-style mailboxes, then the rest.
5. A business found by two searches, two branches linking one page, and branch pages that only repeat emails already
   delivered are **charged once**. One free summary row per search term and location tells you how many places were
   checked, how many had no website, how many websites were read, and how many leads came out.

Measured on 2026-11-03 (from a home connection): **dentists in central Austin: 10 leads from 17 websites read**;
plumbers in Austin: 40 leads, 17 websites without an email and 17 that refused or were down, out of 75 read;
accountants in Leeds: 10 leads from 10 websites.

### Input example

```json
{
    "searchTerms": ["dentist", "orthodontist"],
    "locations": ["Austin, TX", "San Antonio, TX"],
    "radius": 15,
    "distanceUnit": "km",
    "maxLeadsPerSearch": 200,
    "minReviews": 5,
    "onlyNewLeads": true
}
```

### Output example

```json
{
    "rowType": "lead",
    "name": "Toothbar",
    "email": "info@toothbar.com",
    "emails": ["info@toothbar.com"],
    "websitePhones": ["5129498202"],
    "contactPageUrl": "https://toothbar.com/contact/",
    "facebook": "https://www.facebook.com/toothbar",
    "instagram": "https://www.instagram.com/thetoothbar",
    "linkedin": "https://www.linkedin.com/in/tooth-bar-90b898170",
    "x": null,
    "youtube": null,
    "tiktok": null,
    "pagesRead": 2,
    "website": "https://www.toothbar.com/",
    "domain": "toothbar.com",
    "category": "Dentist",
    "categories": ["Dentist", "Cosmetic dentist", "Dental implants provider", "Emergency dental service"],
    "address": "211 Walter Seaholm Dr LR 160, Austin, TX 78701",
    "street": "211 Walter Seaholm Dr LR 160",
    "city": "Austin",
    "postalCode": "78701",
    "state": "Texas",
    "countryCode": "US",
    "phone": "(512) 607-4268",
    "phoneInternational": "+15126074268",
    "rating": 4.9,
    "reviewCount": 735,
    "claimed": true,
    "closed": false,
    "openingHours": ["Monday: 8 AM–5 PM", "Tuesday: 8 AM–5 PM", "Wednesday: 8 AM–5 PM", "Thursday: 8 AM–5 PM", "Friday: Closed", "Saturday: Closed", "Sunday: Closed"],
    "placeId": "ChIJO0xN0RG1RIYRIDFDZYJkYjA",
    "cid": "3486459572745613600",
    "mapsUrl": "https://www.google.com/maps?cid=3486459572745613600",
    "lat": 30.2671177,
    "lng": -97.7527535,
    "distance": 0.93,
    "distanceUnit": "km",
    "searchTerm": "dentist",
    "location": "Austin, TX",
    "scrapedAt": "2026-11-03T12:00:00.000Z"
}
```

#### Lead fields

| field | meaning |
|---|---|
| `rowType` | `lead` (charged), or one of the other rows below |
| `email` | the best address found: on the website's own domain first, then a free mailbox (Gmail, Outlook…), then any other |
| `emails` | every address found on the pages read, best first (up to 10), junk removed |
| `websitePhones` | numbers the website shows (its `tel:` links first, then numbers written on the page; fax numbers left out) |
| `contactPageUrl` | the contact page that was read, or `null` when the site has none the Actor could find |
| `facebook`, `instagram`, `linkedin`, `x`, `youtube`, `tiktok` | the business's profiles linked from its website (share buttons and posts are not profiles); `null` when not linked |
| `pagesRead` | how many pages of the website were read (1–3) |
| `website`, `domain` | the website the Google profile links to (tracking parameters removed), and its host |
| `name`, `category`, `categories` | the business and its Google categories (the first is the main one) |
| `address`, `street`, `city`, `postalCode`, `state`, `countryCode` | as Google Maps shows them; `null` for a service-area business that hides its address |
| `phone`, `phoneInternational` | the Google profile's phone as displayed and in international format |
| `rating`, `reviewCount` | Google stars and number of reviews; `null` when nobody has reviewed it |
| `claimed`, `closed` | `false` when Google offers "Claim this business"; `true` when Google marks the place closed |
| `openingHours` | one line per day, Monday first |
| `placeId`, `cid`, `mapsUrl`, `lat`, `lng`, `distance`, `distanceUnit` | Google's ids, the profile link, the pin, and how far from the location's centre, in your unit (`km` or `mi`) |
| `searchTerm`, `location`, `scrapedAt` | which search found it, and when (UTC) |

#### Other rows

- `rowType: "place"` — only with **Include places without an email**: a business whose website was read and showed no
  email, with the same fields (`email` is `null`) — its phones and social profiles are often still there.
- `rowType: "unreadable"` (free) — only with **Include places without an email**: a business whose website could not be
  read, with its Maps fields, a `reason` (`unreachable`: down, no such domain, an error page; `blocked`: the site
  refused or showed a bot check) and a `message`.
- `rowType: "search"` (free) — one per search term and location: `status` (`ok`, `blocked`, `location_not_found`,
  `not_searched`), `resolvedLocation`, `placesInRadius`, `withoutWebsite`, `filteredOut`, `websitesRead`,
  `withoutEmail`, `unreadableWebsites`, `foundByEarlierSearch`, `sameWebsiteAsEarlier`, `sameEmailsAsEarlier`,
  `deliveredInEarlierRuns`, `leads`, `places`, `maxLeadsReached`, and a `reason` when something stopped it.

The dataset has three views: **Leads**, **Websites not read** and **Searches**.

### Filters (never charged)

- **Phone number required**, **minimum rating**, **minimum reviews**, **categories to keep** (a keyword match on
  Google's categories), **include closed businesses** (off by default). Filtered businesses' websites are not read.
- **Only new leads**: on scheduled runs, skip every business an earlier run already delivered to you; their websites
  are not read again. The list is kept in a key-value store of your own account
  (`steadyscrape-google-maps-email-extractor-state`), saved as the run goes.

### Pricing

Pay per event, **no start fee**:

| event | price |
|---|---|
| `lead` — a business whose own website showed at least one email | **US$0.003** (US$3 per 1,000 leads) |
| `place` — a business whose website showed no email, **only when you turn on "Include places without an email"** | **US$0.001** |

Businesses without a website, filtered businesses, websites that could not be read, duplicates, places outside the
radius, search summaries, unknown locations and blocked searches are **free**. Your run's spending limit is respected:
no website is read once the limit cannot pay for another lead, and the summary row says the run stopped there.

### Related Actors

- **Businesses without a website**: [Google Maps Businesses Without a Website](https://apify.com/steadyscrape/google-maps-no-website-leads)
  sells the other half of a Google Maps search — the places this Actor leaves out — for web designers.
- **German, Austrian and Swiss companies**: [Impressum Scraper](https://apify.com/steadyscrape/impressum-scraper) reads
  the legal notice of DACH websites for the company's register number, VAT ID and managing directors.

### When Google asks for verification

The Actor reads the same answers Google Maps gives its own web page, one search at a time. **If Google asks an IP for
verification** — its "unusual traffic" page, a CAPTCHA, a JavaScript check, or any answer that is not Google Maps'
normal data — **that IP is done for the run**: nothing more is sent from it, and the run goes on from another IP of
Apify's datacenter proxy. It never solves, bypasses or replays a challenge. Plain overloads (HTTP 429 or 5xx without a
challenge) take the same way. When every IP the run can use has been refused, the searches left are free `blocked` rows
that say so, and the run still finishes cleanly with the leads found so far. Websites are read at a visitor's
pace (at least half a second between two requests to one site); a website that refuses gets one more try from another IP, and one that shows a
bot check (Cloudflare's "Just a moment…", DataDome and the like) is never retried — it is a free row.

### Limits

- Up to 20 search terms and 20 locations per run; radius 0.5–50 km (or miles); up to 5,000 leads per search.
- Only what the website shows in its HTML: an address drawn only by JavaScript, shown as an image or behind a contact
  form is not found. Websites of big chains often have no email at all (only a form).
- `websitePhones` can include a number that is not the business's own (a licensing board some states require in the
  footer); the Google profile's `phone` is the business's.
- Location names resolve as Google Maps resolves them: an ambiguous or misspelt name can land on Google's closest
  guess, even a business in another state. Check `resolvedLocation` in the search row, add the state, or give
  `latitude, longitude`. The `country` setting biases short names.

### Is it legal?

It reads publicly visible Google Maps listings and the public pages of the businesses' own websites — the addresses
and numbers businesses publish so customers can reach them — without logging in. Use the leads in line with the laws
that apply to your outreach (for example CAN-SPAM, GDPR, CASL, PECR), and honour opt-outs. This Actor is not affiliated
with, endorsed by or sponsored by Google.

# Actor input Schema

## `searchTerms` (type: `array`):

The kind of business, as you would type it into Google Maps, one per line: <code>dentist</code>, <code>plumber</code>, <code>roofing contractor</code>, <code>accountant</code>. Each term is searched in every location. Up to 20.

## `locations` (type: `array`):

Where to look, one per line: a city, area, postcode or address as you would type it into Google Maps (<code>Austin, TX</code>, <code>Shoreditch, London</code>, <code>78701</code>), or <code>latitude, longitude</code>. Up to 20. A business found by two searches is delivered and charged once.

## `radius` (type: `number`):

How far from each location's centre to look (0.5 to 50, in the unit below). Businesses outside it are left out, never charged. The radius is covered with a grid of searches, so it reaches past the ~120–240 results one Google Maps search shows.

## `distanceUnit` (type: `string`):

No description

## `maxLeadsPerSearch` (type: `integer`):

Stop each search term in each location after this many leads with an email (1 to 5,000). The run's spending limit is respected too: no website is read once it cannot pay for another lead.

## `includePlacesWithoutEmail` (type: `boolean`):

Off: only businesses whose website showed an email are delivered (<code>lead</code>). On: businesses whose website was read but showed no email are delivered too, at the lower <code>place</code> price, with their phones and social profiles; and websites that could not be read (down, refusing, a bot check) come as free rows with the reason.

## `requirePhone` (type: `boolean`):

Keep only businesses whose Google profile shows a phone number. Others are not read and not charged.

## `minRating` (type: `number`):

Keep only businesses rated at least this many stars (0 to 5). Empty: any rating, including none.

## `minReviews` (type: `integer`):

Keep only businesses with at least this many reviews. Empty: any.

## `categoryFilter` (type: `array`):

Keep only businesses with a Google category containing one of these words (any case), e.g. <code>dentist</code>. Empty: every category the search returns.

## `includeClosed` (type: `boolean`):

Off: places Google Maps marks as closed are left out.

## `onlyNewLeads` (type: `boolean`):

Skip businesses an earlier run of this Actor already delivered to you (remembered in a storage of your own account): their websites are not read and they are not charged. Turn it on for scheduled runs.

## `language` (type: `string`):

Language of categories and opening hours, and the language websites are asked for, as a code: <code>en</code>, <code>es</code>, <code>de</code>, <code>pt-BR</code>.

## `country` (type: `string`):

Two-letter country code Google answers for (<code>us</code>, <code>gb</code>, <code>au</code>, <code>ca</code>); it also decides which city a short location name means.

## Actor input object example

```json
{
  "searchTerms": [
    "dentist"
  ],
  "locations": [
    "Austin, TX"
  ],
  "radius": 10,
  "distanceUnit": "km",
  "maxLeadsPerSearch": 10,
  "includePlacesWithoutEmail": false,
  "requirePhone": false,
  "includeClosed": false,
  "onlyNewLeads": false,
  "language": "en",
  "country": "us"
}
```

# Actor output Schema

## `rows` (type: `string`):

No description

## `summary` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "searchTerms": [
        "dentist"
    ],
    "locations": [
        "Austin, TX"
    ],
    "radius": 10,
    "maxLeadsPerSearch": 10
};

// Run the Actor and wait for it to finish
const run = await client.actor("steadyscrape/google-maps-email-extractor").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "searchTerms": ["dentist"],
    "locations": ["Austin, TX"],
    "radius": 10,
    "maxLeadsPerSearch": 10,
}

# Run the Actor and wait for it to finish
run = client.actor("steadyscrape/google-maps-email-extractor").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "searchTerms": [
    "dentist"
  ],
  "locations": [
    "Austin, TX"
  ],
  "radius": 10,
  "maxLeadsPerSearch": 10
}' |
apify call steadyscrape/google-maps-email-extractor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,steadyscrape/google-maps-email-extractor"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/Q3AZeRwB6TFkCiyJf/builds/DYDWdP1sH2zzLupuG/openapi.json
