# Google Maps Leads Scraper: Emails, Phones & Decision Makers (`datagleaner/google-maps-leads`) Actor

Google Maps leads scraper. Search local businesses by category and city; get name, address, phone, website, rating, plus emails, phones, socials and named decision makers (owner, CEO, managers) read from each business website. $0.0015 per place.

- **URL**: https://apify.com/datagleaner/google-maps-leads.md
- **Developed by:** [Data Gleaner](https://apify.com/datagleaner) (community)
- **Categories:** Lead generation, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

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 Leads Scraper: emails, phones and decision makers for local businesses

Find local business leads on Google Maps and get the people behind them. Give it a category and a city (`dentist in Austin, TX`, `marketing agency in Denver, CO`) and it returns every business with **name, category, address, phone, website, rating, review count and coordinates**, then reads each business's own website for **emails, phone numbers, social profiles and the named decision makers** (owner, founder, CEO, managers) with their titles and published emails. One row per business. From $0.0015 per place; you pay for contacts and people only when they are found.

**To try it:** click Start with the prefilled input (10 dentists in Austin, about 15 seconds), or with an empty input to run a built-in example (5 places with website contacts).

### What it does

1. **Searches Google Maps** for each search you give it, 20 places per page, until it has `maxPlacesPerSearch` new places or Google has no more (about 150 to 210 places for a big city and a common category). Places seen in an earlier search of the same run are skipped, so overlapping searches do not bill twice.
2. **Adds website contacts** (on by default): reads the business's home page and its contact, about and legal pages and returns the emails on the business's own domain (best first, with the page each came from), phone numbers in international format, social profiles and the contact page link.
3. **Adds decision makers** (optional): reads the business's team, about, leadership and Impressum pages and returns named people with job title, seniority, LinkedIn link when the site shows it, and the email the site publishes for that person. Optionally it adds an **inferred email** for people with no published one (see below).

Every business that Google returns gets a row with a clear `status`, so a website that is down or blocked never makes a lead disappear: the Maps fields are still there and the enrichment fields say why they are empty.

### Use cases

- B2B lead lists: every marketing agency, law firm, dentist or roofer in a city with a phone, an email and the owner's name.
- Local sales outreach: find businesses with no website (`onlyWithWebsite` off, then filter `website` empty) or with a poor rating.
- Market mapping and territory planning: count and locate competitors by category with coordinates.
- CRM enrichment: fill phone, website, email and owner for a list of local accounts.
- AI agents that need "local businesses of type X in city Y, with a way to contact them" as a single tool call.

### Input

```json
{
  "searches": [
    {"query": "marketing agency", "location": "Denver, CO"},
    "plumber in Austin, TX"
  ],
  "maxPlacesPerSearch": 60,
  "includeWebsiteContacts": true,
  "includeDecisionMakers": true,
  "inferEmails": false,
  "maxPeoplePerPlace": 3
}
```

- `searches`: free text (`plumber in Austin, TX`) or `{query, location}` objects. One search returns up to about 200 places, so split large areas into cities or neighbourhoods.
- `maxPlacesPerSearch`: new places per search (default 20, up to 1000; Google itself stops at about 200). `maxItems`: hard cap for the whole run.
- `includeWebsiteContacts` (default on), `includeDecisionMakers` (default off), `inferEmails` (default off), `maxPeoplePerPlace` (default 3, most senior first), `seniority` filter.
- `onlyWithWebsite`: leave out places that list no website (not returned, not billed).
- `language`, `country`: Google's language and regional bias. `maxPagesPerWebsite`, `maxTeamPages`, `maxConcurrency`, `delaySecs`, `respectRobotsTxt` (default on) and `proxyConfiguration` (default none).

### Output

One row per business, `people` nested.

```json
{
  "placeId": "ChIJ4wjr7dK3RIYRk3qS0wU4Aqk",
  "name": "Peaks Digital Marketing | Denver SEO",
  "category": "Marketing agency",
  "address": "1300 S Broadway, Denver, CO 80210",
  "city": "Denver",
  "latitude": 39.6,
  "longitude": -104.98,
  "phone": "(720) 432-7115",
  "website": "https://www.peaksdigitalmarketing.com/",
  "rating": 4.9,
  "reviewCount": 120,
  "mapsUrl": "https://www.google.com/maps/place/?q=place_id:ChIJ4wjr7dK3RIYRk3qS0wU4Aqk",
  "status": "ok",
  "contactsStatus": "ok",
  "peopleStatus": "ok",
  "bestEmail": "info@peaksdigitalmarketing.com",
  "emails": [{"value": "info@peaksdigitalmarketing.com", "kind": "generic", "foundOn": "https://www.peaksdigitalmarketing.com/contact"}],
  "phones": ["+17204327115"],
  "socialLinks": {"linkedin": ["https://www.linkedin.com/company/peaks-digital-marketing"]},
  "contactPageUrl": "https://www.peaksdigitalmarketing.com/contact",
  "people": [
    {"name": "David A. Finberg", "title": "CEO & Founder", "seniority": "founder", "linkedinUrl": null, "email": null,
     "emailInferred": false, "pattern": null, "evidence": null, "confidence": null,
     "sourceUrl": "https://www.peaksdigitalmarketing.com/about"}
  ],
  "searchRank": 1,
  "input": "marketing agency in Denver, CO",
  "scrapedAt": "2026-10-15T12:30:00+00:00"
}
```

(Illustrative values: the place ID, address, coordinates and rating in your dataset come straight from Google.)

- `status`: `ok`; `noWebsite` (you asked for enrichment but the place lists no website); `enrichmentFailed` (its website was unreachable or blocked, the Maps fields are still valid).
- `contactsStatus` and `peopleStatus` give the detail: `ok`, `noContacts`, `noPeople`, `noTeamPage`, `noWebsite`, `socialOrDirectoryWebsite` (the listed website is a Facebook, Instagram, Yelp or similar page, which is not crawled), `unreachable`, `blockedByRobots`, `off` or `error`.
- A per-search summary (pages read, new places, status, `blocked` when Google refused) is saved in the key-value store as `SEARCH_SUMMARY`.

### Inferred emails (optional)

With `includeDecisionMakers` and `inferEmails` on, a person whose email the site does not publish may get a best guess in `email`, with `emailInferred: true`, the `pattern` (for example `first.last`), the `evidence` (up to 3 published addresses that fit the pattern) and a `confidence` (`high` with two or more consistent published examples, `medium` with one). The pattern is learned only from personal addresses the business itself publishes, so a site with no published personal address gets no guess. Only names that read as a real person get a guess. A published email is never replaced, and the guess is not verified with a mail server: some will bounce. There is no extra charge.

### How much does it cost?

Pay per event, no start fee:

| Event | Price | When it is charged |
| --- | --- | --- |
| `place` | $0.0015 | each business returned |
| `website-contacts` | $0.002 | only for a place where at least one email or phone was found on its website |
| `decision-maker` | $0.002 | each named person returned |

Worked example: 60 marketing agencies in Denver with contacts and up to 3 decision makers each. On the test run, 50 of 60 websites gave an email or phone and 95 people were found: 60 x $0.0015 + 50 x $0.002 + 95 x $0.002 = **$0.38**. The same 60 places as a plain list (contacts and decision makers off) cost **$0.09**. Websites that are unreachable, and places with no team page, add no charge. For comparison, the largest Maps Actor lists $0.003 per place, $0.002 for contact details and $0.005 per lead on its public price list (checked 2026-10-15).

Platform usage is small: the test run above used about $0.009 of compute for 60 places with every option on.

### Limits

- **About 150 to 210 places per search.** Google's results end there for one query. Search by city, neighbourhood or category to cover more.
- **Unofficial source.** Places come from the same public JSON that Google Maps itself loads; Google can change or throttle it. The Actor paces requests (one at a time, 0.5 s apart), backs off on a rate limit, switches to a fresh Apify Proxy session after two in a row and stops with a clear status if Google keeps refusing. It never solves captchas.
- **Websites only.** Contacts and people come from the business's own site. A business whose "website" is a Facebook page, or whose team page loads with JavaScript, returns no people. Typical rates are in the FAQ.
- **No reviews, photos or popular-times.** This Actor is for leads.
- Large runs with decision makers take minutes: websites are crawled politely, one request at a time per site.

### FAQ

**How many leads have an email?** On a 60-place run of marketing agencies in Denver: 97% had a website, 93% a phone, 72% an email found on the website and 57% at least one named person. Dentists in Austin: 100% website and phone, 60 to 80% email. Your category and country will differ.

**Does it return the owner's name?** When the business's team, about or Impressum page names them, with the title the site gives. Small businesses often have no team page, so expect people on roughly half the places with a website.

**Can I run many cities at once?** Yes, add one search per city. Duplicates across searches are removed by `placeId`.

**Does it work outside the US?** Yes: set `language` and `country` and put the city in the search. Address field order follows what Google shows.

**Why is a place marked `enrichmentFailed`?** Its website did not answer, answered with an error, or blocks bots. The business and its Maps fields are still returned and billed as one place; no contact or person event is charged.

### Use with Python

```python
## pip install apify-client
from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("datagleaner/google-maps-leads").call(run_input={
    "searches": [{"query": "dentist", "location": "Austin, TX"}],
    "maxPlacesPerSearch": 50, "includeDecisionMakers": True})
for row in client.dataset(run.default_dataset_id).iterate_items():
    print(row["name"], row["phone"], row["bestEmail"], [p["name"] for p in row.get("people", [])])
```

### Use with JavaScript / Node.js

```js
// npm install apify-client
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('datagleaner/google-maps-leads').call({
    searches: [{ query: 'dentist', location: 'Austin, TX' }],
    maxPlacesPerSearch: 50,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const row of items) console.log(row.name, row.phone, row.bestEmail);
```

### Use it from n8n, Make, Zapier or an AI agent

Actor ID: `datagleaner/google-maps-leads`

Minimal input:

```
{"searches": ["dentist in Austin, TX"], "maxPlacesPerSearch": 20}
```

Each tool below runs this Actor with your own Apify API token.

- **n8n:** add the **Apify** node (`@apify/n8n-nodes-apify`), choose **Run an Actor and get dataset**, set Actor to `datagleaner/google-maps-leads` and paste the input above.
- **Make:** use the Apify app's **Run an Actor** module, then **Get Dataset Items**.
- **Zapier:** use the Apify action **Run Actor**, then **Fetch dataset items**.
- **AI agents (MCP):** connect to `https://mcp.apify.com/?tools=datagleaner/google-maps-leads`. In Claude Code:

  ```
  claude mcp add --transport http apify "https://mcp.apify.com/?tools=datagleaner/google-maps-leads"
  ```

  Then ask in plain words, for example:

  > Find 30 marketing agencies in Denver with their website email and the name of the owner or CEO.

### Use over plain HTTP

```bash
curl -X POST "https://api.apify.com/v2/acts/datagleaner~google-maps-leads/run-sync-get-dataset-items?token=<APIFY_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"searches": ["dentist in Austin, TX"], "maxPlacesPerSearch": 10}'
```

### Related Actors

- [Website Contact Details Scraper](https://apify.com/datagleaner/website-contact-details-scraper): emails, phone numbers and social profiles from any list of websites, when you already have the sites.
- [Team Page Contacts](https://apify.com/datagleaner/team-page-contacts): named people, titles and LinkedIn from company domains, when you already have the domains.
- [Company Enrichment by Domain](https://apify.com/datagleaner/company-enrichment-by-domain): domain to company profile (name, logo, official social profiles, contacts, founding date).
- [Tech Stack by Domain](https://apify.com/datagleaner/tech-stack-by-domain): which platforms and tools a website runs, to qualify the leads you found.

### Responsible use

This Actor reads publicly available business listings and the public pages businesses publish about themselves. Names, titles, phone numbers and work emails are still personal data: you are responsible for complying with the law that applies to you, including GDPR, CCPA, telemarketing and anti-spam rules, and Google's terms for the data you collect, when you store the results or contact the people in them.

# Actor input Schema

## `searches` (type: `array`):

What to find on Google Maps. Each entry is either free text such as `plumber in Austin, TX` or an object `{"query": "plumber", "location": "Austin, TX"}`. Add one entry per category and city: one search returns up to about 200 places, so split big areas into neighbourhoods or cities. Leave empty to run a small built-in example.

## `maxPlacesPerSearch` (type: `integer`):

Stop each search after this many new places. Google Maps returns 20 places per page and about 200 in total for one search. Every place returned is one billed `place` event.

## `includeWebsiteContacts` (type: `boolean`):

Read each place's own website (home page and its contact, about and legal pages) for emails, phone numbers, social profiles and the contact page link. A `website-contacts` event is billed only for places where an email or phone is found.

## `includeDecisionMakers` (type: `boolean`):

Read the website's team, about, leadership and Impressum pages for named people (owner, founder, CEO, managers) with title, seniority, LinkedIn link and published email. Billed per person returned (`decision-maker` event). Places with no team page are not charged for this.

## `inferEmails` (type: `boolean`):

Off by default. With decision makers on, people with no published email also get a best-guess `email` with `emailInferred: true`, the `pattern` and the published `evidence` addresses it was learned from, and a `confidence`. The pattern is learned only from personal addresses the site itself publishes. The guess is NOT verified. A published email is never replaced. No extra charge.

## `maxPeoplePerPlace` (type: `integer`):

Most senior first (founders, then C-level, VPs, directors, board, managers, others). 0 means no limit. Each person returned is one billed event.

## `seniority` (type: `array`):

Return only these levels. Leave empty for everyone. People filtered out are not billed.

## `onlyWithWebsite` (type: `boolean`):

Leave out places that list no website (they are not returned and not billed). The place limit counts only the places returned.

## `maxItems` (type: `integer`):

Hard cap on dataset rows for the whole run, across all searches.

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

Two-letter language code for Google Maps (`en`, `de`, `fr`, `es`, `ja`...). Affects category names and address format.

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

Two-letter country code Google uses to rank results (`us`, `gb`, `de`, `au`...). Put the city in the search itself; this only sets the regional bias.

## `maxPagesPerWebsite` (type: `integer`):

Pages read per website for contacts, home page included. Contact-looking pages are tried first.

## `maxTeamPages` (type: `integer`):

Team, about, leadership and Impressum pages read per website for decision makers, after the home page.

## `maxConcurrency` (type: `integer`):

How many places have their website crawled at the same time. Requests to Google Maps are always sent one at a time.

## `delaySecs` (type: `number`):

Pause between two requests to Google Maps, and between two requests to the same website.

## `requestTimeoutSecs` (type: `integer`):

How long to wait for each website page before retrying.

## `respectRobotsTxt` (type: `boolean`):

Skip website pages that the site's robots.txt disallows.

## `proxyConfiguration` (type: `object`):

Optional. Defaults to no proxy. If Google answers two requests in a row with a rate limit, the run switches to a fresh Apify Proxy session by itself.

## Actor input object example

```json
{
  "searches": [
    {
      "query": "dentist",
      "location": "Austin, TX"
    }
  ],
  "maxPlacesPerSearch": 10,
  "includeWebsiteContacts": true,
  "includeDecisionMakers": false,
  "inferEmails": false,
  "maxPeoplePerPlace": 3,
  "onlyWithWebsite": false,
  "maxItems": 1000,
  "language": "en",
  "country": "us",
  "maxPagesPerWebsite": 6,
  "maxTeamPages": 4,
  "maxConcurrency": 10,
  "delaySecs": 0.5,
  "requestTimeoutSecs": 20,
  "respectRobotsTxt": true
}
```

# Actor output Schema

## `results` (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 = {
    "searches": [
        {
            "query": "dentist",
            "location": "Austin, TX"
        }
    ],
    "maxPlacesPerSearch": 10
};

// Run the Actor and wait for it to finish
const run = await client.actor("datagleaner/google-maps-leads").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 = {
    "searches": [{
            "query": "dentist",
            "location": "Austin, TX",
        }],
    "maxPlacesPerSearch": 10,
}

# Run the Actor and wait for it to finish
run = client.actor("datagleaner/google-maps-leads").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 '{
  "searches": [
    {
      "query": "dentist",
      "location": "Austin, TX"
    }
  ],
  "maxPlacesPerSearch": 10
}' |
apify call datagleaner/google-maps-leads --silent --output-dataset

```

## MCP server setup

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

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/WGbWm77gwt8zvuRRh/builds/GaUKkdEM3vQvcRfxA/openapi.json
