# Google Maps Scraper - Emails, Phones, Reviews & Leads (`shadowextractor/google-maps-scraper`) Actor

Scrape Google Maps by search phrase, URL or place ID. Get emails, phones, websites, social profiles, ratings, reviews, opening hours, photos, search rank and lead scores - in clean, export-ready columns, with a visual run report.

- **URL**: https://apify.com/shadowextractor/google-maps-scraper.md
- **Developed by:** [Shadow Extractor](https://apify.com/shadowextractor) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 place scrapeds

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## 🗺 Google Maps Scraper — Leads, Emails, Phones & Reviews

Turn any Google Maps search, URL or place ID into **clean, export-ready business data**.

Paste what you want, press Start, get a spreadsheet you can actually use: business name, category, rating, review count, phone, website, **public email addresses**, social profiles, address broken into fields, coordinates, opening hours, photos, search rank and ready-made lead scores.

No Playwright, no browser, no captchas to solve — the actor talks to Google Maps' own data endpoints, so it is fast and cheap.

***

### ✨ What makes this one different

| | |
|---|---|
| **One smart input box** | Search phrases, Maps URLs, place IDs and data IDs all go into the *same* field. The actor detects each type automatically. |
| **Three depth levels** | ⚡ Quick, 📇 Contacts, 🔎 Full — one switch, no fiddling with ten checkboxes. |
| **Column presets** | ✨ Essentials, 📇 Leads, ⭐ Reviews, 🗂 Everything. Your CSV is readable the moment it downloads, with stable headers in a fixed order. |
| **📊 Visual run report** | Every run writes an HTML dashboard to the key-value store: coverage %, pipeline health, top categories and per-search-term results. Open it straight from the run's **Output** tab. |
| **Smart deduplication** | Beyond place ID: collapse chains by **domain** or **phone number** so you get one row per company. |
| **Real filters** | Rating, review count, include/exclude keywords, website presence, "must have a phone or email", open-now, closed businesses. |
| **Handy deep links** | `whatsappLink`, `directionsUrl`, `reviewsUrl` — generated for every row. |
| **Flat text columns** | `emailsText`, `phonesText`, `categoriesText`, `photoUrlsText` — no more JSON arrays inside your spreadsheet cells. |

***

### 🚀 Quick start

1. Put your targets into **🔍 What do you want to scrape?**
   ```
   coffee shops in New York, NY
   dentists in Austin, TX
   https://www.google.com/maps/place/Blue+Bottle+Coffee/...
   ChIJN1t_tDeuEmsRUsoyG83frY4
   ```
2. Leave everything else on its default and press **Start**.
3. Download the dataset as CSV / Excel / JSON, and open the **📊 Run report** for a summary.

Need emails? Switch **⚙️ How deep should we go?** to **📇 Contacts** and set **📊 Which columns** to **📇 Leads**.

***

### ⚙️ Data depth

| Mode | What you get | Speed |
|---|---|---|
| ⚡ **Quick** | Places, categories, ratings, Google-published phone, address, coordinates, website, photos, opening state, search rank | Fastest, cheapest |
| 📇 **Contacts** | Everything above **+ public emails and social profiles** crawled from each business website (up to 3 pages, hard time budget) | Medium |
| 🔎 **Full** | Everything above **+ place detail page + Google reviews** with aggregated review statistics | Slowest, richest |

***

### 🎚 Filters

All optional, all combined with AND, all applied **before** the row is saved:

- **⭐ Minimum Google rating** and **💬 minimum number of reviews**
- **✅ Keep only these categories** — keyword match on category and title
- **🚫 Drop places matching these words** — cut chains, franchises, noise
- **🌐 Website presence** — with / without a website (the classic agency lead list)
- **📇 Contact requirement** — must have a phone, an email, either, or both
- **⛔ Skip closed businesses** and **🟢 only places open right now**

***

### 📊 Output columns

Pick a preset in **📊 Which columns do you want?**

| Preset | Columns | Best for |
|---|---|---|
| ✨ **Essentials** | 23 | A clean CSV to eyeball or hand to someone |
| 📇 **Leads** | 39 | Cold outreach: emails, phones, socials, WhatsApp links, lead scores |
| ⭐ **Reviews** | 37 | Reputation monitoring: star split, owner-reply rate, review recency |
| 🗂 **Everything** | ~150 | Analysis and pipelines — nothing is dropped |

Fixed presets always emit the **same columns in the same order**, even when a value is missing, so appended CSV exports line up across runs.

#### Scores the actor computes for you

| Field | Meaning |
|---|---|
| `leadScore` | 0–100 — how usable this row is as a sales lead |
| `contactCompletenessScore` | 0–100 — how much contact data was found |
| `digitalPresenceScore` | 0–100 — website, email, socials, description, photos |
| `reputationScore` | 0–100 — rating, review volume, owner responsiveness |
| `outreachOpportunityScore` / `outreachPriority` | 0–100 + high/medium/low, with `outreachReasons` explaining why |
| `intelligenceConfidence` | How much to trust an *absence* of contact data (`high` / `medium` / `partial`) |

***

### 🧹 Deduplication

Places are always de-duplicated by Google place ID. Choose a stricter rule when you want **one row per company**:

- **🌐 …plus website domain** — collapses every branch that shares a site
- **☎️ …plus phone number** — collapses shared phone lines
- **🌐☎️ …plus domain or phone** — strictest

***

### 🚀 Speed & cost

| Setting | Parallel searches | Use when |
|---|---|---|
| 🐢 Gentle | low | You see blocked requests, or you're on a weak proxy |
| ⚖️ Balanced | medium | Default — good for almost everything |
| 🚀 Turbo | high | Large keyword lists on residential proxies |

Use **🛑 Stop after this many places** to put a hard cap on a run, and **🔢 Max places per search term** to control per-keyword depth.

> **Proxies:** residential proxies are strongly recommended. Google Maps blocks datacenter IPs quickly.

***

### 📤 Output records

| Record | What it is |
|---|---|
| **Dataset** | One row per business, shaped by your column preset |
| **`REPORT`** (key-value store) | 📊 Visual HTML run report — coverage, pipeline health, per-term results |
| **`OUTPUT`** (key-value store) | 🧾 JSON summary — counters, timings, warnings, dataset link |

#### Sample row (✨ Essentials)

```json
{
  "title": "Blue Bottle Coffee",
  "categoryName": "Coffee shop",
  "rating": 4.4,
  "reviewsCount": 1837,
  "priceLevel": "$$",
  "phone": "(510) 653-3394",
  "website": "https://bluebottlecoffee.com/",
  "email": "hello@bluebottlecoffee.com",
  "address": "300 Webster St, Oakland, CA 94607",
  "city": "Oakland",
  "state": "CA",
  "postalCode": "94607",
  "countryCode": "US",
  "latitude": 37.8007,
  "longitude": -122.2758,
  "businessStatus": "open",
  "openingHoursToday": "7 AM–5 PM",
  "imageUrl": "https://lh5.googleusercontent.com/...",
  "searchQuery": "coffee shops in Oakland, CA",
  "searchRank": 3,
  "placeId": "ChIJ...",
  "googleMapsUrl": "https://www.google.com/maps/place/?q=place_id:ChIJ...",
  "scrapedAt": "2026-09-03T10:21:44Z"
}
```

***

### 🛟 Mistakes are explained, not crashed on

A wrong setting never kills the run. The actor repairs what it can, keeps scraping, and tells you exactly what it did — in the run log, in the **Input check** section of the 📊 run report, and in `OUTPUT.inputIssues`.

| You do this | What happens |
|---|---|
| Paste a Yelp or Facebook link | That entry is skipped: *"is a link, but not a Google Maps link"* — the rest of your list still runs |
| Require an email in ⚡ Quick mode | Warning: *"Quick mode never collects emails — switch to 📇 Contacts or 🔎 Full, otherwise every place will be filtered out"* |
| Require an email **and** only places without a website | Warning: emails come from websites, so this combination returns nothing |
| Put the same word in both keep and drop lists | Warning: exclusions win |
| Send a number where a list was expected, an unknown option, or an out-of-range value | Repaired to a sane value, with the valid options listed |
| Turn the proxy off | Warning: Google blocks datacenter IPs quickly |

When a run saves nothing, the run **status message** says *why* — it repeats the specific problem instead of a generic "nothing found".

***

### ❓ FAQ

**Does it scrape emails from Google Maps?**
Google Maps does not publish emails. In **📇 Contacts** and **🔎 Full** modes the actor visits the business's own website and collects the email addresses published there.

**Why did I get fewer places than my limit?**
Google returns a finite result set per search — a narrow query in a small town simply has fewer businesses. Broaden the phrase, drop the location, or add more search terms.

**Can I re-enrich a list I already have?**
Yes. Paste your Google Maps URLs, place IDs or data IDs into the same input field and pick a deeper mode.

**Is this legal?**
The actor only collects publicly visible information. You are responsible for how you use it — in particular for complying with GDPR/CCPA and anti-spam rules when contacting businesses.

***

### 🛠 Local development

```bash
pip install -r requirements.txt
python -m pytest tests -q      # unit tests, no network
apify run                      # run locally with ./storage/key_value_stores/default/INPUT.json
apify push                     # deploy
```

# Actor input Schema

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

Paste anything — the actor detects the type for you:<br><br>• <b>Search phrase</b> — <code>dentists in Austin, TX</code><br>• <b>Google Maps URL</b> — <code>https://www.google.com/maps/place/...</code><br>• <b>Place ID</b> — <code>ChIJN1t\_tDeuEmsRUsoyG83frY4</code><br>• <b>Data / feature ID</b> — <code>0x89c25a31:0x6b1b1c1f</code><br><br>One item per line. Duplicate businesses are saved only once.

## `locationQuery` (type: `string`):

Optional. A city, district, address, ZIP or country appended to plain search phrases (never to URLs or IDs). Leave empty when your searches already name a place.

## `mode` (type: `string`):

Each level adds requests, data and runtime. Start with <b>Quick</b> to validate your query, then re-run deeper.

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

Upper limit of unique businesses collected for each search phrase. URLs and place IDs always return exactly one place.

## `maxImages` (type: `integer`):

How many photo URLs to keep. Set 0 to skip photos entirely and shrink the output.

## `maxReviews` (type: `integer`):

Used in <b>🔎 Full</b> mode. Reviews are attached to their place row, together with aggregated stats: average, star split, owner-response rate and recency.

## `reviewsSort` (type: `string`):

Order Google returns reviews in. Use <i>Lowest rating</i> to mine complaints, <i>Newest</i> to monitor reputation.

## `categoryFilterWords` (type: `array`):

A place is kept when its Google category or title contains <b>at least one</b> of these words. Leave empty to keep everything.

## `excludeKeywords` (type: `array`):

A place is removed when its title, category or address contains any of these words. Great for cutting franchises, chains or irrelevant categories.

## `placeMinimumStars` (type: `number`):

Keep places rated at or above this value. <code>0</code> disables the filter and keeps unrated places too.

## `minReviewsCount` (type: `integer`):

Filter out businesses with fewer reviews than this — a fast way to keep only established places.

## `website` (type: `string`):

<i>Only without a website</i> is the classic lead list for web-design and marketing agencies.

## `requireContact` (type: `string`):

Keep only rows you can actually reach out to. Email filtering needs <b>📇 Contacts</b> or <b>🔎 Full</b> mode.

## `skipClosedPlaces` (type: `boolean`):

Exclude places Google marks as permanently or temporarily closed.

## `openNowOnly` (type: `boolean`):

Keep only businesses Google reports as open at the moment of the run.

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

Language Google Maps answers in. Changes category names, opening-hours text and the review language mix.

## `countryCode` (type: `string`):

Google localises results by country. Pick the market you are selling into.

## `fieldPreset` (type: `string`):

Shapes every dataset row so your CSV/Excel export is readable out of the box. Fields always come out in a fixed, logical order.

## `deduplicateBy` (type: `string`):

Places are always de-duplicated by Google place ID. Add a stricter rule to collapse multi-location chains or shared phone lines.

## `speed` (type: `string`):

Higher speed sends more parallel requests. <b>Turbo</b> is best for large keyword lists on residential proxies; drop to <b>Gentle</b> if you see blocked requests.

## `maxTotalPlaces` (type: `integer`):

Hard budget for the whole run. <code>0</code> means no global cap. Useful to keep costs predictable on huge keyword lists.

## `proxy` (type: `object`):

Residential proxies are strongly recommended — Google Maps blocks datacenter IPs quickly.

## Actor input object example

```json
{
  "searchTerms": [
    "coffee shops in New York, NY"
  ],
  "locationQuery": "Berlin, Germany",
  "mode": "fast",
  "maxPlacesPerSearch": 20,
  "maxImages": 5,
  "maxReviews": 10,
  "reviewsSort": "relevant",
  "categoryFilterWords": [],
  "excludeKeywords": [],
  "placeMinimumStars": 0,
  "minReviewsCount": 0,
  "website": "allPlaces",
  "requireContact": "any",
  "skipClosedPlaces": false,
  "openNowOnly": false,
  "language": "en",
  "countryCode": "US",
  "fieldPreset": "essentials",
  "deduplicateBy": "placeId",
  "speed": "balanced",
  "maxTotalPlaces": 0,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  }
}
```

# Actor output Schema

## `report` (type: `string`):

A visual dashboard of this run: coverage, pipeline health and per-search-term results.

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

Machine-readable counters, timings and warnings for this run.

## `overview` (type: `string`):

No description

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

No description

## `reviews` (type: `string`):

No description

## `location` (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": [
        "coffee shops in New York, NY"
    ],
    "mode": "fast",
    "maxPlacesPerSearch": 20,
    "maxImages": 5,
    "maxReviews": 10,
    "language": "en",
    "countryCode": "US",
    "fieldPreset": "essentials",
    "speed": "balanced",
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "US"
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("shadowextractor/google-maps-scraper").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": ["coffee shops in New York, NY"],
    "mode": "fast",
    "maxPlacesPerSearch": 20,
    "maxImages": 5,
    "maxReviews": 10,
    "language": "en",
    "countryCode": "US",
    "fieldPreset": "essentials",
    "speed": "balanced",
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "US",
    },
}

# Run the Actor and wait for it to finish
run = client.actor("shadowextractor/google-maps-scraper").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": [
    "coffee shops in New York, NY"
  ],
  "mode": "fast",
  "maxPlacesPerSearch": 20,
  "maxImages": 5,
  "maxReviews": 10,
  "language": "en",
  "countryCode": "US",
  "fieldPreset": "essentials",
  "speed": "balanced",
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  }
}' |
apify call shadowextractor/google-maps-scraper --silent --output-dataset

```

## MCP server setup

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

```

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/K5UGSDbXnc7x8jwJY/builds/5Ks5cmi25RuFlHEfq/openapi.json
