# Google Maps Leads Scraper – Emails, Phones & Socials (`simple.actor/google-maps-leads`) Actor

Business leads from Google Maps: every place for a search plus the emails, phone numbers and social accounts its own website publishes, one row per business. Name, rating, address, hours and Place ID included. No API key, no browser. Unofficial.

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

## Pricing

from $0.50 / 1,000 business returneds

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 & Socials

Business leads from Google Maps, with the contact details Maps does not show.
Type what you would type into Maps — "dentists in miami fl", "plumbers in
london", "wedding photographers in lisbon" — and every business Google lists
comes back as one row: name, category, rating, review count, address, phone
and website from Maps, **plus the email addresses, phone numbers and social
accounts its own website publishes**, and the contact form if it has one.

One run replaces the usual two-step job of scraping Maps and then crawling
every website in the list.

**No API key, no login, no browser, no account of yours involved.**

**$0.50 per 1,000 businesses, plus $1.50 per 1,000 for each business whose
website gave contacts** — so a lead with contacts costs $0.002, and a typical
20-business search about $0.04. Apify platform usage included, nothing else to
pay. Never charged: a search that could not be read, a business your searches
already delivered, and a website that publishes nothing, could not be read or
was not reached.

*Unofficial. Not affiliated with, endorsed by, or sponsored by Google.*

### Features

- **Emails from the business's own website** — the home page and the pages
  likeliest to carry them (contact, imprint, about, team), best address first:
  one on the business's own domain, found on a contact page.
- **Phone numbers in international format** — the Maps number and the ones the
  website publishes, as `+13055550100`, read under the business's own country
  so a London "020 …" comes back `+44`.
- **Social accounts as their own columns** — Facebook, Instagram, LinkedIn,
  X, YouTube, TikTok and WhatsApp, ready for a CSV; every other platform found
  is in `social`.
- **A status on every row** — `contactsStatus` says whether contacts were
  found, the site publishes none, there is no website, or the site could not
  be read and why. Filter on it.
- **Each business once** — a business that two of your searches both find is
  delivered once, and a chain whose branches share one website has it read
  once.
- **Everything Google lists** — paged automatically to the end of Google's
  results, typically 120–140 businesses a search.
- **Google's Place ID on every row**, plus coordinates, opening hours,
  categories and a Maps link.
- **Careful about wrong answers.** Phone numbers and emails are read from what
  the page shows and its `mailto:`/`tel:` links, never from the page's code —
  where dates, prices and image ids look like phone numbers. An address left on
  the page by the site's website builder or one of its plugins is not reported
  as the business's. A Facebook or Instagram page given as a business's
  "website" is returned as that account, not read as a site.
- **Runs stay green.** A search that cannot be read becomes an error row, and a
  website that cannot be read is a status on its business's row.

### Input

```json
{
  "queries": ["dentists in miami fl", "dentists in fort lauderdale fl"],
  "maxPlaces": 20,
  "maxPagesPerSite": 4,
  "skipPlacesWithoutWebsite": false,
  "language": "en",
  "country": "US"
}
```

| Field | Type | Default | What it does |
|---|---|---|---|
| `queries` | array | — | Maps searches, one per line, **with the place in them** (`plumbers in denver co`). Links copied from Maps work too. |
| `maxPlaces` | integer | `20` | Businesses per search, up to 200. Google runs out at ~120–140. |
| `maxPagesPerSite` | integer | `4` | Website pages read per business, 1–10. 1 reads the home page only and finds about half as many emails. |
| `skipPlacesWithoutWebsite` | boolean | `false` | Leave out, and do not charge for, businesses whose listing has no website. |
| `reviewCounts` | boolean | `true` | Re-read a Maps page Google answered without review counts. |
| `language` | string | `en` | What Google answers in: `en`, `de`, `fr`, `es`, `ja`, `pt-BR`… |
| `country` | string | `US` | Two-letter code: the country Google reads the search from. Set it to the place's country — it decides which city a shared name means. |

**Put the place in the search.** Google answers "plumbers" alone for wherever
it thinks the request came from, which is a data centre, not you. To get more
businesses in one area, split the search by neighbourhood or by a narrower
category.

**Name the country when a city's name exists in more than one.** Google
reads "dentists in manchester" through `country`, which defaults to US, so it
can come back as Manchester, New Hampshire, as Manchester, England, or as a
mix of the two that changes from one page of results to the next. Measured
with the default: 8 of 20 pages were all New Hampshire and 10 mixed both;
with `country` set to `GB`, all 20 were England. Birmingham (Alabama) and
London (Ontario, Ohio) mixed in the same way. Write "dentists in manchester
uk", or set `country` to the place's country. Every row's `countryCode` says
where that business is.

### Output

One item per business, in the run's dataset:

```json
{
  "type": "place",
  "position": 6,
  "name": "ONE Dental Miami",
  "placeId": "ChIJOeo2vFu22YgRnXmUhbsc1LE",
  "address": "1330 Coral Wy #100, Miami, FL 33145",
  "countryCode": "US",
  "phone": "(305) 285-5150",
  "phoneE164": "+13052855150",
  "website": "http://www.onedentalmiami.com/",
  "rating": 4.8,
  "reviewCount": 371,
  "category": "Dentist",
  "contactsStatus": "found",
  "email": "info@onedentalmiami.com",
  "emails": [
    "info@onedentalmiami.com"
  ],
  "websitePhones": [
    "+13052855150",
    "+13052855174"
  ],
  "facebook": "https://www.facebook.com/onedentalm",
  "instagram": "https://www.instagram.com/onedentalmiami",
  "linkedin": null,
  "twitter": null,
  "youtube": "https://www.youtube.com/@ONEDentalMiami",
  "tiktok": null,
  "whatsapp": null,
  "social": {
    "facebook": "https://www.facebook.com/onedentalm",
    "youtube": "https://www.youtube.com/@ONEDentalMiami",
    "instagram": "https://www.instagram.com/onedentalmiami"
  },
  "messaging": {},
  "contactFormUrl": "https://www.onedentalmiami.com/contact-our-dental-office/",
  "domain": "onedentalmiami.com",
  "websitePagesRead": 4,
  "contactsError": null,
  "query": "dentists in miami fl",
  "retrievedAt": "2026-10-18T16:54:36.634Z"
}
```

Every row also carries `cid`, `categories`, `latitude`, `longitude`,
`openingHours`, `imageUrl`, `googleMapsUrl`, `searchUrl` and `page`.

| `contactsStatus` | What it means |
|---|---|
| `found` | The website gave at least one email, phone number or social account. |
| `none_found` | The website was read and publishes none of them — often true of small businesses. |
| `no_website` | The Maps listing has no website. The Maps phone and address are still there. |
| `social_profile` | The listing's "website" is a social account; it is filled into that column, and nothing is read. |
| `unreadable` | The website could not be read — down, refusing automated readers, or too slow. `contactsError` says which. |
| `not_read` | The run reached its own time limit before this website. |

A search that could not be read is its own row with `type: "error"`, an
`error` code (`bad_input`, `read_failed`, `timed_out` or `parse_failed`) and an
`errorDescription`. The run stays green either way and its status message
counts them, so if you run on a schedule, check for `type: "error"` rows
rather than the run status.

An empty dataset means every search was read and Google Maps lists no
businesses for it.

#### Runs that reach their time limit

A run stops shortly before its own timeout and keeps everything it has
delivered — rows go out as their websites are read, not at the end. A search
it never reached gets a `timed_out` row, and a business whose website it had
no time for arrives with `contactsStatus: "not_read"`. A 20-business search
takes about a minute; raise the timeout or split the run for long lists.

When a run does fail, this Actor reports the failure to its author so it can be
fixed: the error and where in the code it happened, how long the run had been
going and what it had delivered, and the shape of the input — how many
searches, how deep, which language and country. Your search terms and your
results are never part of that report.

### How to use

#### From Apify Console

1. Put one search per line into **Searches**, with the place in it.
2. Set **Businesses per search** — Google has 120–140 for most searches.
3. Click **Start**, then open the **Dataset** tab: the **Leads** view is one
   line per business with its email, phone and accounts; export as CSV or
   Excel.

#### From the API

```bash
curl -s "https://api.apify.com/v2/acts/simple.actor~google-maps-leads/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"queries": ["dentists in miami fl"], "maxPlaces": 60}'
```

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

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('simple.actor/google-maps-leads').call({
    queries: ['dentists in miami fl', 'dentists in fort lauderdale fl'],
    maxPlaces: 100,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
const withEmail = items.filter((row) => row.email);
```

#### From Claude

Claude can run this Actor for you through Apify's MCP server. This connector
URL gives it this Actor, Website Contacts, Google Ads Transparency and LinkedIn
Jobs, and nothing else:

```
https://mcp.apify.com/?tools=simple.actor/google-maps-leads,simple.actor/website-contacts,simple.actor/google-ads-transparency,simple.actor/linkedin-jobs
```

On claude.ai or the desktop app, go to Customize → Connectors → + Add → Add
custom connector, paste the URL and sign in to Apify; in Claude Code, run
`claude mcp add --transport http lead-tools "<url>"`, then `/mcp` to sign in.
Runs are charged to your Apify account at the prices above.

A tested prompt:

```
Use the Google Maps leads tool to find dentists in Miami, FL. Ask for 40 businesses so that at least 20 have an email. Wait until the run has finished before you read the results. Then give me a table of the ones with an email: name, email, phone, website, rating.
```

It found 40 dentists, 21 of them with an email, in 110 seconds, for $0.074.
Ask for about twice the businesses you need, because about half publish an
email, and tell Claude to wait: a read before the run ends returns a partial
list that looks complete. The
[full guide](https://github.com/simple-actor/n8n-claude-lead-generation/tree/main/claude/google-maps-leads)
has the transcript and what to check before you email anyone.

### Use cases

- **Cold outreach** — every business of a kind in an area, with an email to
  write to and a number to call.
- **Agency prospecting** — local businesses with no website, or with no
  Instagram, are the ones to pitch.
- **CRM enrichment** — the same Place ID, phone and accounts for businesses
  already on your list.
- **Market research** — how many competitors a category has in a city, how
  they rate, and how they present themselves online.

### FAQ

**Why does a business have no email?** Many small businesses publish only a
phone number and a contact form; `contactsStatus` tells "the site has none"
(`none_found`) from "the site could not be read" (`unreadable`).

**Why is a website `unreadable`?** Some sites refuse automated readers, some
are down, and some take longer than 40 seconds. The reason is in
`contactsError`.

**Does it read pages behind a login, or run a browser?** No. It reads what a
website serves to any visitor, which is where businesses publish their
contact details.

**Is it legal?** It reads business contact details that businesses publish
publicly for the purpose of being contacted. You are responsible for how you
use them, including the rules on unsolicited messages where you and your
recipients are.

# Actor input Schema

## `queries` (type: `array`):

What to search Google Maps for, one search per line, as you would type it into Maps. Put the place in the search — "dentists in miami fl", "plumbers in denver co" — because a search with no place in it is answered for wherever Google thinks the request came from, and name the country when a city's name exists in several ("dentists in manchester uk"). A business that two of your searches both find is delivered and charged once.

## `maxPlaces` (type: `integer`):

How many businesses to return per search. Google lists 20 per request and runs out at roughly 120–140 for most searches, where Maps itself stops scrolling — asking for more returns everything Google has. To get more businesses in one area, split the search: by neighbourhood ("dentists in brickell miami", "dentists in coral gables"), or by a more specific category.

## `maxPagesPerSite` (type: `integer`):

How many pages of each business's website are read for contacts. The first is the home page; the rest are the site's own links ranked by how likely each is to carry contact details — a contact page, an imprint, an about or team page. Most small businesses publish their email on the contact page, not the home page, so 1 finds roughly half as many emails. The price is the same whatever you set; a higher number only makes the run slower.

## `skipPlacesWithoutWebsite` (type: `boolean`):

On, a business whose Google Maps listing has no website is not delivered and not charged. Off (the default), it is delivered with its Maps phone and address and contactsStatus "no_website".

## `reviewCounts` (type: `boolean`):

Google answers some requests with a lighter record that carries the star rating but not the number of reviews or the opening hours. On, such a page is re-read until a complete one arrives, which nearly always works and costs a few extra seconds. Off, the first answer is kept and reviewCount and openingHours may be null on some rows.

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

The language Google answers in — place names, categories and opening hours: en, de, fr, es, ja, pt-BR… Defaults to en.

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

The country Google searches from, as a two-letter code: US, GB, DE, FR, JP… Set it to the country of the place you search: a city name shared by several countries is read for this one, so with the default US, "dentists in manchester" comes back as Manchester, New Hampshire, or mixed with Manchester, England, page by page. It also nudges which places rank first. Defaults to US.

## Actor input object example

```json
{
  "queries": [
    "dentists in miami fl",
    "plumbers in denver co"
  ],
  "maxPlaces": 20,
  "maxPagesPerSite": 4,
  "skipPlacesWithoutWebsite": false,
  "reviewCounts": true,
  "language": "en",
  "country": "US"
}
```

# Actor output Schema

## `leads` (type: `string`):

One row per business: name, category, email, phone, website, social accounts, rating and address.

## `contacts` (type: `string`):

Every email, phone number and account found on each business's website.

## `raw` (type: `string`):

Every field of every item, exactly as pushed.

# 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 = {
    "queries": [
        "dentists in miami fl"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("simple.actor/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 = { "queries": ["dentists in miami fl"] }

# Run the Actor and wait for it to finish
run = client.actor("simple.actor/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 '{
  "queries": [
    "dentists in miami fl"
  ]
}' |
apify call simple.actor/google-maps-leads --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,simple.actor/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/LaMituYJcUhkZdJqY/builds/l6jo4QCcEOv13CWaZ/openapi.json
