# Google Maps Scraper – Multi-City Lead Lists with Emails (`locomint/google-maps-multi-city-scraper`) Actor

Google Maps leads scraper for multi-city lead lists. Enter business categories and cities to get one deduplicated list with phone, website, rating, hours, role emails, social profiles and WhatsApp from each site. Whole-city grid coverage, free filters, and you pay only for the leads returned.

- **URL**: https://apify.com/locomint/google-maps-multi-city-scraper.md
- **Developed by:** [Locomint](https://apify.com/locomint) (community)
- **Categories:** Lead generation, Marketing, Business
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.20 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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 – Multi-City Lead Lists with Emails

This Google Maps leads scraper takes a list of business categories and a list of cities and returns one deduplicated lead list: each business's listing phone, website, rating and opening hours, plus the role mailboxes, social profiles and WhatsApp number published on its own website. Website contacts are included, and you pay only for the leads returned.

```json
{
  "categories": ["dentist", "orthodontist"],
  "cities": ["Manchester, United Kingdom", "Leeds, United Kingdom"],
  "maxPlacesPerCombination": 100
}
```

### What it does

Every city and category pair is one search, so two categories in three cities make six searches, run city by city. A run takes up to 25 categories and 50 cities and runs the first 100 pairs; pairs beyond 100 are skipped with a warning in the log, so split bigger jobs across runs.

Each search collects up to `maxPlacesPerCombination` places (500 at most). A plain map search runs dry at about 100 to 120 results per area. Past that the actor splits the city into a grid of up to 64 cells and searches each cell, ending early once three cells in a row add fewer than two new places. Results outside the resolved city are dropped, so a search in Lagos, Nigeria never hands you restaurants in Lagos, Portugal.

The list is deduplicated across the whole run by `place_id`. A clinic that matches both "dentist" and "orthodontist", or sits where two cities' areas overlap, is delivered and charged once, and its `search_city` and `search_category` columns name the pair that found it first.

Each lead then gets two more reads, both included in the price:

- **The full listing** (`includeDetails`): description, opening hours for all seven days, timezone, plus code, photo count with up to five photo URLs, and attributes such as delivery or dine-in.
- **The business website** (`includeEnrichment`): the homepage, plus one hop to the contact page when the homepage is missing an email address or a WhatsApp number. It returns role mailboxes, social profile links (Facebook, Instagram, LinkedIn, X, YouTube, TikTok), a WhatsApp number from click-to-chat links, the contact page URL, the technologies the site runs on (about 35 signatures) and a `website_status`.

The four filters are free and run on the search result before either read, so a lead that fails a filter is never fetched and never charged.

#### What each row contains

| Group | Columns |
|---|---|
| Found by | `search_city`, `search_category` |
| Identity | `place_id`, `cid`, `name`, `primary_category`, `categories`, `description` |
| Location | `address`, `street`, `city`, `region`, `postal_code`, `country_code`, `lat`, `lng`, `plus_code`, `timezone`, `service_area_business` |
| Listing contact | `phone` (as displayed), `phone_e164`, `website` |
| Reputation | `rating`, `review_count`, `price_level` |
| Status and hours | `status`, `open_now`, `hours`, `hours_status` |
| Media and attributes | `photos_count`, `photo_urls`, `attributes` |
| From the website | `emails`, `socials`, `whatsapp`, `contact_form_url`, `tech_stack`, `website_status`, `enriched_at` |
| Provenance | `source`, `fetched_at` |

`website_status` is one of `ok`, `unreachable` (the site was down or refused the request), `parked` (a domain-for-sale page), `redirect_social` (the "website" is a Facebook or Instagram page, kept in `socials`) or `no_website`. `hours` is keyed by weekday, each day a list of `{"open": "09:00", "close": "18:00"}` periods in local 24-hour time, with an empty list for a closed day.

### Who it is for

- **Lead generation.** Build a territory in one run: every physiotherapist in twelve cities, with phone, role mailbox and WhatsApp in the same row and no duplicates to clean up.
- **Sales prospecting.** Split the result by `search_city` and give each rep their patch. `rating`, `review_count` and `open_now` separate busy businesses from quiet ones before anyone picks up the phone.
- **Agencies.** `websiteFilter: "without"` across a region lists every business in a category with no website, and `website_status: "parked"` finds the lapsed domains.
- **Local SEO.** Benchmark a multi-location client against the competition in each of its cities: rating, review count, categories and photo count side by side.
- **CRM enrichment.** Load a new territory into your CRM with `place_id` as the dedupe key, so the next run updates records instead of duplicating them.
- **Market research.** Compare how many businesses of a category each city has, and how they rate, from one dataset with `lat` and `lng` for mapping.

### How to use it

1. Open the actor in Apify Console and click **Try for free**.
2. Enter **business categories**, one per line: `dentist`, `physiotherapist`.
3. Enter **cities** with the country, one per line: `Manchester, United Kingdom`, `Leeds, United Kingdom`. "Warsaw" alone can resolve to Warsaw, Indiana; "Warsaw, Poland" cannot.
4. Set **max leads per city x category** and any filters.
5. Click **Start** and export the dataset to CSV, Excel or JSON, or read it through the API.

The synchronous endpoint runs the actor and returns the rows in one request:

```bash
curl -X POST "https://api.apify.com/v2/acts/locomint~google-maps-multi-city-scraper/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"categories": ["dentist"], "cities": ["Manchester, United Kingdom", "Leeds, United Kingdom"], "maxPlacesPerCombination": 25}'
```

That endpoint waits at most 300 seconds, which is about 100 leads with details on. For bigger lists use the Python client (`pip install apify-client`), which starts the run and waits for it to finish:

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("locomint/google-maps-multi-city-scraper").call(run_input={
    "categories": ["dentist", "orthodontist"],
    "cities": ["London, United Kingdom", "Manchester, United Kingdom"],
    "maxPlacesPerCombination": 200,
    "websiteFilter": "with",
})
for lead in client.dataset(run.default_dataset_id).iterate_items():
    print(lead["search_city"], lead["name"], lead["phone_e164"], lead["emails"])
```

### Input example

```json
{
  "categories": ["dentist", "orthodontist"],
  "cities": [
    "London, United Kingdom",
    "Manchester, United Kingdom",
    "Birmingham, United Kingdom"
  ],
  "maxPlacesPerCombination": 200,
  "placeMinimumStars": "4",
  "websiteFilter": "with",
  "skipClosedPlaces": true,
  "categoryFilterWords": ["dent", "orthodont"],
  "includeDetails": true,
  "includeEnrichment": true,
  "language": "en"
}
```

That is six searches of up to 200 leads each, so at most 1,200 leads.

| Field | Default | What it does |
|---|---|---|
| `categories` | required | Up to 25. Each category is searched in every city. |
| `cities` | required | Up to 50, each with its country. At most 100 city x category pairs run per run. |
| `maxPlacesPerCombination` | 20 | Cap per city x category pair, 1 to 500. |
| `placeMinimumStars` | any | `"2"` to `"4.5"`. Places with no rating are left out. |
| `websiteFilter` | `any` | `with` or `without` a website. |
| `skipClosedPlaces` | false | Leave out places marked temporarily or permanently closed. |
| `categoryFilterWords` | – | Keep a place only if its categories contain one of these words (case-insensitive). |
| `includeDetails` | true | Read the full listing. Same price either way. When off, `hours` is empty and `hours_status` reads `not_published`. |
| `includeEnrichment` | true | Visit each business website. Same price either way. |
| `language` | `en` | Language for names and categories (`en`, `de`, `ar`, `ja`, …). |

### Output example

A lead from a `restaurant` search in `Lisbon, Portugal`, trimmed to the columns most people use first:

```json
{
  "place_id": "ChIJ…",
  "name": "Belcanto",
  "primary_category": "Fine dining restaurant",
  "categories": ["Fine dining restaurant", "Portuguese restaurant", "Restaurant"],
  "phone": "+351 21 342 0607",
  "phone_e164": "+351213420607",
  "website": "https://belcanto.pt/",
  "rating": 4.6,
  "review_count": 6697,
  "status": "open",
  "address": "Largo de São Carlos 10, 1200-410 Lisboa, Portugal",
  "city": "Lisbon",
  "country_code": "PT",
  "lat": 38.7099,
  "lng": -9.1418,
  "emails": ["press@belcanto.pt", "work@belcanto.pt"],
  "socials": {
    "facebook": "https://www.facebook.com/belcantolisboa",
    "instagram": "https://www.instagram.com/belcantolisboa",
    "linkedin": null,
    "x": null,
    "youtube": null,
    "tiktok": null
  },
  "whatsapp": null,
  "contact_form_url": "https://belcanto.pt/contact/",
  "tech_stack": ["wordpress", "react", "google-analytics"],
  "website_status": "ok",
  "search_category": "restaurant",
  "search_city": "Lisbon, Portugal"
}
```

Missing values are `null` or an empty list, never guessed.

### Pricing

This actor is pay-per-event. You pay for two events and nothing else; Apify platform usage and proxy traffic are not billed to you separately.

| Event | Price | Charged |
|---|---|---|
| Lead delivered | $0.0015 ($1.50 per 1,000) until 26 September 2026, then $0.0030 ($3.00 per 1,000) | once per row in the dataset |
| Actor start | $0.00005 per GB of run memory | once per run ($0.00005 at 1 GB) |

**Worked example.** 1,000 dentists across London, Manchester and Birmingham: 1,000 x $0.0015 = $1.50, plus $0.00005 for the start. From 26 September 2026 the same run is $3.00.

The most a run can cost is cities x categories x `maxPlacesPerCombination` x the price per lead, and most runs cost less, because you are not charged for:

- **Filtered-out places.** The filters run on the search result, before the listing and the website are read, and a place that fails is never written to the dataset.
- **Duplicates.** A business found by two categories or in two overlapping cities is delivered and charged once.
- **Empty searches.** A pair that finds nothing costs nothing beyond the start event.

Set a maximum cost per run in the run options and the actor stops before the next row would pass it. Apify's free plan includes $5 of usage a month with no card.

### FAQ

**How many leads will one run return?**
Up to `maxPlacesPerCombination` for each city and category pair, less filtered places and duplicates. Common categories in big cities reach the cap, helped by the grid sweep past the first 100 to 120 results; niche categories return what exists.

**What happens when a business matches two categories?**
It is delivered once, and charged once. `search_category` and `search_city` record the pair that found it first, in the order the searches ran: city by city, and within a city in the order you listed the categories.

**Why must every city include the country?**
A bare city name is resolved to whichever place the map picks first. In our tests "Warsaw" came back as Warsaw, Indiana, while "Warsaw, Poland" cannot be misread.

**Why do some leads have no email?**
The business has no website, the site did not answer, or it publishes no role mailbox on its homepage or contact page. A 403 or a challenge page counts as a refusal, and the actor does not retry it through another IP. Mailboxes that name a person are dropped on purpose, and `contact_form_url` still gives you the site's contact form.

**Does it include reviews?**
No. Every lead carries `rating` and `review_count`, but not review text. The `reviews_summary` column exists for compatibility and is empty in this actor.

**Can I schedule it and get only new businesses?**
The actor does not remember earlier runs: each run delivers, and charges for, everything it finds. Schedule it in Apify and compare `place_id` against your previous dataset to see which businesses are new and which have disappeared.

**Is it legal to scrape Google Maps for leads?**
The actor reads publicly available listing pages and business websites while logged out, never signs in, never solves a CAPTCHA, and backs off when a site refuses it. How you use the list is your responsibility: cold outreach to businesses is regulated by laws such as GDPR, CAN-SPAM and PECR, and the rules differ by country.

**Which countries does it cover?**
Any country with map listings, and cities from different countries can share one run. There is no country targeting; each city you type decides where its searches run.

### Limits

- **Run size.** 25 categories, 50 cities and 100 city x category pairs per run; 500 leads per pair.
- **Speed.** Listing reads are paced at about 20 a minute, so a run with details on delivers roughly 20 leads a minute. The 1,200-lead example above takes about an hour.
- **No name matching.** This form has no option to match business names against the search term. For brand searches use the Google Maps Scraper & Email Extractor below.
- **Reviews.** No review text. `reviews_summary`, `snapshot_id` and `first_seen_at` are always empty in actor runs.
- **Websites.** Pages are read as served, without running JavaScript, so a contact page that only appears after scripts load is missed. The actor reads the homepage and at most one contact page. A site that takes longer than 20 seconds is abandoned: that row keeps its `website` but shows `website_status: "no_website"` with `enriched_at: null`, meaning the site was not read.
- **Emails.** Role mailboxes only, such as info@, sales@ and bookings@. An address on another domain, a Gmail account for example, is kept only when its name is a business function.
- **Phones.** `phone` and `phone_e164` come from the listing, not from the website. `phone_e164` is empty when only a national number was offered and the calling code cannot be settled safely (+1 and +7 are each shared by several countries).
- **Fields not yet seen filled.** `price_level` has not been populated in any record we have captured, and closed markers have not appeared in our captures either, so `skipClosedPlaces` has not yet been tested against a real closed listing.
- **Addresses.** Service-area businesses with no storefront have an empty `address` and `service_area_business: true`; `lat` and `lng` are still set. Australian localities come through with the state attached (`"Melbourne VIC"`).

### Compliance

Business contact points only: role mailboxes published on the business's own website. Mailboxes that name a person are dropped and owner names are never collected. You are responsible for using the data lawfully (for example GDPR, CAN-SPAM and PECR for outreach).

Questions, bug reports and feature requests go on this actor's Issues tab. Business owners can send data-removal requests to info@locomint.io; that address is for removal requests only. This actor keeps no copy of what it reads between runs, so each run returns what the public pages show at that moment.

Not affiliated with or endorsed by Google.

### Other Locomint actors

- [Google Maps Scraper & Email Extractor – Business Leads](https://apify.com/locomint/google-maps-scraper): One city at a time, with name matching and place-ID input.
- [Google Maps Place Details Scraper – Bulk Place ID Lookup](https://apify.com/locomint/google-maps-place-details-scraper): Place IDs or place-page links in, full records out.
- [Website Email Scraper – Contact Details, Socials & WhatsApp](https://apify.com/locomint/website-content-contact-extractor): Contact points from website URLs you supply.
- [Website Content Crawler – Markdown for AI, Emails & Contacts](https://apify.com/locomint/website-crawler-content-contacts): A whole site as Markdown, with its contact points.
- [Bulk Email Verifier & Validator](https://apify.com/locomint/bulk-email-verifier): Checks whether addresses can receive mail.
- [Company Enrichment API – Domain to Emails, Socials & Tech](https://apify.com/locomint/company-enrichment): A domain in, its contacts and technologies out.
- [AI Crawler Checker – robots.txt Rules for GPTBot & ClaudeBot](https://apify.com/locomint/ai-crawler-checker): Which AI crawlers a site's robots.txt allows.
- [Schema Markup Validator & Generator – JSON-LD Checker](https://apify.com/locomint/schema-markup-validator): Checks and generates schema.org markup.

# Actor input Schema

## `categories` (type: `array`):

What kind of businesses, e.g. "dentist", "gym", "car rental". Each category is searched in every city. Up to 25.

## `cities` (type: `array`):

One per line, always with the country: "Lisbon, Portugal", "Dubai, UAE". Up to 50 cities; up to 100 city x category searches per run.

## `maxPlacesPerCombination` (type: `integer`):

Upper limit per search. You pay per lead delivered; a lead matching two categories is delivered and charged once.

## `placeMinimumStars` (type: `string`):

Only deliver places rated at least this. Places without a rating are skipped. Free - filtered places are not charged.

## `websiteFilter` (type: `string`):

Only places with a website (good for enrichment) or only places without one (good for web-design outreach).

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

Leave out places marked temporarily or permanently closed.

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

Keep only places whose category contains at least one of these words, e.g. \["dentist", "orthodontist"]. Case-insensitive.

## `includeDetails` (type: `boolean`):

Adds description, weekly opening hours, plus code, timezone, photos and attributes for each place. Included in the price.

## `includeEnrichment` (type: `boolean`):

Visits each business's own website to extract business emails, social profiles, WhatsApp, contact page and tech stack. Included in the price. Never collects personal data.

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

BCP-47 language for names and categories (en, ar, de, ja, ...).

## Actor input object example

```json
{
  "categories": [
    "dentist"
  ],
  "cities": [
    "Lisbon, Portugal"
  ],
  "maxPlacesPerCombination": 5,
  "placeMinimumStars": "",
  "websiteFilter": "any",
  "skipClosedPlaces": false,
  "includeDetails": true,
  "includeEnrichment": true,
  "language": "en"
}
```

# Actor output Schema

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

One row per business, deduplicated across cities and categories: identity, location, contact, reputation, hours, website enrichment, plus search\_city and search\_category.

# 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 = {
    "categories": [
        "dentist"
    ],
    "cities": [
        "Lisbon, Portugal"
    ],
    "maxPlacesPerCombination": 5
};

// Run the Actor and wait for it to finish
const run = await client.actor("locomint/google-maps-multi-city-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 = {
    "categories": ["dentist"],
    "cities": ["Lisbon, Portugal"],
    "maxPlacesPerCombination": 5,
}

# Run the Actor and wait for it to finish
run = client.actor("locomint/google-maps-multi-city-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 '{
  "categories": [
    "dentist"
  ],
  "cities": [
    "Lisbon, Portugal"
  ],
  "maxPlacesPerCombination": 5
}' |
apify call locomint/google-maps-multi-city-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,locomint/google-maps-multi-city-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/76Vzti9jKM6PENPju/builds/WcFyEYGj4gt45x12Z/openapi.json
