# Google Maps Email Extractor - Contact Details & Leads Scraper (`s-r/google-maps-contact-details`) Actor

Extract emails, phone numbers and social profiles of local businesses found on Google Maps. Search by category and city and get name, address, rating, reviews, website and opening hours, plus the contacts published on each business website. For lead generation, sales outreach and local research.

- **URL**: https://apify.com/s-r/google-maps-contact-details.md
- **Developed by:** [SR](https://apify.com/s-r) (community)
- **Categories:** Lead generation, Business
- **Stats:** 277 total users, 64 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.27 / 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.
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?

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 - Contact Details Scraper

This Google Maps email extractor finds local businesses on Google Maps and collects the email addresses, phone numbers and social profiles published on their own websites. Search for a business category in a city, and each row gives you the place name, address, phone number, rating, review count, website and opening hours, plus the contact details found on that website. Use the dataset to build a local lead list, prepare sales outreach, review directory information or research potential customers in a specific city.

Each result is a place returned by Google Maps for your search. Contact enrichment adds information found on the place's own website. A website can have an email address, a social profile, both or neither. Empty contact fields remain empty when the public page does not provide that information. The Actor does not infer an email address from a business name or domain. This Actor was previously listed as Google Maps Email & Contact Details Scraper; the Actor URL, input and output are unchanged.

### How the Google Maps email extractor works

1. You enter one or more search phrases, such as `dentist Utrecht` or `coffee shops amsterdam`, optionally with separate location fields.
2. The Actor runs each phrase as a Google Maps search and reads the places in result order, up to your limit per query.
3. Your filters (rating, reviews, website, category, name matching, closed places) remove places that do not match.
4. For each remaining place with a website, the Actor opens the homepage and up to two relevant contact pages and extracts email addresses, website phone numbers and links to Facebook, Instagram, LinkedIn, X (Twitter), YouTube, TikTok and Pinterest.
5. Every place is written to the dataset as one row, whether or not contact details were found.

### Input: Google Maps contact details searches

Provide one or more search phrases in `queries`. Include a business category and a location, or combine a category with the separate location fields. This is the input used for a successful run:

```json
{"queries": ["coffee shops amsterdam"], "max_results_per_query": 20}
```

`max_results_per_query` defaults to 120 and accepts values from 1 to 400. The limit applies to each query. Google can return fewer places than the limit, particularly for a narrow area or category. A larger limit requests more results; it cannot make an unavailable business appear in Google's response.

Set `location_query`, `city`, `state`, `postal_code` or `country_code` to add a location to your search. These values are appended in address order, while text already present in the query is skipped. The returned `search_query` shows the exact composed phrase. `gl` defaults to `us` and controls the search region; it does not replace the geographic words in your query. `hl` defaults to `en` and controls the language of categories, opening hours and other display labels.

Set `scrape_contacts` to false when you only need the Maps information. It defaults to true.

Two older settings keep their names and defaults, so saved tasks and API calls continue to work unchanged. `use_cookies` defaults to true and lets the Actor open Google Maps with a fresh session, which returns the full set of place attributes and review counts; leave it on unless you are running a small test. `use_proxy` defaults to false and is still accepted, but it no longer changes the results: every run uses the same request routing. The optional `cursor` field is a continuation token for paged API access. A run already continues through all result pages on its own, so you normally leave it empty; if you do set it, keep the other input fields unchanged.

### Filters and result counts

Use `minimum_stars` for a minimum rating of 3, 3.5, 4 or 4.5. `minimum_reviews` sets the lowest review count you want to keep. A place without a supplied rating or review count cannot pass the corresponding minimum. When Google's response omits a required filter field for all relevant results, the run reports that condition rather than claiming that every place had a low rating.

The `website` option accepts `any`, `with_website` or `without_website`. `category_filter_words` keeps places whose categories include at least one supplied word. `search_matching` supports `all`, `only_includes` and `only_exact`: the latter modes compare the place name with terms from the composed search phrase, including location terms. `skip_closed_places` removes places marked permanently closed. A temporarily closed place can remain in the results.

Filters apply after searching, so a filtered dataset can be smaller than the requested limit. The summary includes `placesScanned`, `placesRejectedByFilters` and `filtersApplied`, alongside the delivered result and enrichment counts. Use those fields to distinguish a restrictive filter from a search with few available places.

### Output and exports

The default dataset contains one row per place, in query order and then result order. The same place can appear in more than one query. Core fields include `name`, `place_id`, `fid`, `kgmid`, `address`, `neighborhood`, `street`, `city`, `postal_code`, `state`, `country_code`, `lat`, `lng`, `category`, `categories`, `rating`, `reviews_count`, `phone`, `website`, `website_domain`, `search_position` and `search_query`. `neighborhood` contains the neighborhood supplied by Google, or null when it is unavailable.

Opening information is returned in `current_status` and `opening_hours`, with a day label and hours text for each supplied day. `additional_info` contains Google's grouped attributes. Fields such as `price_range`, `plus_code` and `hours_status` remain available and can be null when the response does not supply a usable value.

When contact enrichment is enabled, rows also contain `emails`, `phones_on_website`, `contact_page_url`, `website_crawled`, `facebook`, `instagram`, `linkedin`, `twitter`, `youtube`, `tiktok` and `pinterest`. Emails are lowercase and deduplicated. The Actor checks the homepage and up to two relevant contact pages. A failed website request keeps the Maps place available, with empty contact fields.

If Google lists a social profile as the place's website, that URL stays in `website`. Social profiles are skipped during the website crawl, so `website_crawled` remains false and the contact fields remain empty. Internal platform links are excluded from contact results. This does not generate a contact enrichment charge. Public links to business profiles found on a business website can still appear in the social fields.

This excerpt comes from an actual run with the input `{"queries": ["dentist"], "location_query": "Utrecht", "country_code": "NL", "hl": "nl", "gl": "nl", "minimum_stars": "4", "minimum_reviews": 20, "website": "with_website", "max_results_per_query": 15}` and several other filters, with a selection of the returned fields shown:

```json
{
  "search_position": 5,
  "name": "Dental Clinics Utrecht Maliebaan",
  "place_id": "ChIJdbHxEE1vxkcRKMHDEZqzNWw",
  "address": "Dental Clinics Utrecht Maliebaan, Maliebaan 44, 3581 CS Utrecht",
  "city": "Utrecht",
  "postal_code": "3581 CS",
  "country_code": "NL",
  "lat": 52.0921853,
  "lng": 5.1341021,
  "category": "Tandarts",
  "rating": 4.6,
  "reviews_count": 474,
  "phone": "030 231 0003",
  "website_domain": "dentalclinics.nl",
  "search_query": "dentist Utrecht NL",
  "emails": ["maliebaan@dentalclinics.nl"],
  "phones_on_website": ["+31302310003", "+319008602"],
  "website_crawled": true,
  "facebook": "https://www.facebook.com/DentalClinicsNL/?locale=nl_NL",
  "instagram": "https://www.instagram.com/dentalclinicsnl",
  "linkedin": "https://linkedin.com/company/dental-clinics-nederland",
  "youtube": "https://www.youtube.com/channel/UC5FI8ARmy_GYNt3lT0JSb2Q",
  "twitter": null,
  "tiktok": null
}
```

In that run, 26 places were scanned, 11 were rejected by the filters and 15 were delivered; 14 of the 15 had at least one email address or social profile on their website. A place without contact details looks like this excerpt from the prefill run (`coffee shops amsterdam`):

```json
{
  "name": "Boerejongens",
  "place_id": "ChIJG8scpJUJxkcR4ccjAgpLST4",
  "address": "Boerejongens, Utrechtsestraat 21, 1017 VH Amsterdam, Netherlands",
  "rating": 4.5,
  "reviews_count": 5778,
  "phone": "+31 20 447 3557",
  "website": "https://www.boerejongens.com/",
  "emails": [],
  "facebook": null,
  "instagram": null,
  "website_crawled": true
}
```

Download the dataset as JSON, CSV or Excel through Apify. JSON preserves the nested opening-hours and attribute structures. The key-value store contains the `summary` record (also stored as `OUTPUT`) and an `errors` record when individual searches could not be completed. The summary includes `warnings` and `partial`: when a business website could not be checked completely, `warnings` names that website and `partial` is true, while every place row is still delivered. A contact link that returns 404 or 410 is treated as a missing page and does not make the run partial. A search with no matching places can finish successfully with an empty dataset.

### Use cases

- **Local lead generation**: build a list of dentists, plumbers, restaurants or agencies in a city, with an email address and phone number for each business that publishes one.
- **Sales outreach**: combine `emails`, `phone` and `linkedin` to reach a business through the channel it actually uses, and use `rating` and `reviews_count` to prioritise.
- **Market and competitor research**: count businesses per category and area, compare ratings and review volumes, and see which competitors have a website or social presence.
- **Directory and CRM enrichment**: match on `place_id` or `website_domain` to update addresses, opening hours and contact channels in an existing database.
- **Agency prospecting**: use `website` set to `without_website` to find businesses that may need a website, and the rating and review fields to see which ones have an established reputation.

### Google Maps email extractor compared with other options

| Option | Maps place data | Emails and social profiles | Effort |
|---|---|---|---|
| Copying from Google Maps by hand | yes, one place at a time | only after visiting each website yourself | very high |
| A general Google Maps scraper | yes | usually not, or as a separate step | low, but a second tool is needed for contacts |
| Browser extensions | partly, limited to what is visible on screen | varies | medium, tied to your browser session |
| This Actor | yes, including hours and attributes | yes, from the business's own website, in the same row | low: one input, one dataset, API and schedules available |

You pay per place and per enriched place, not per page or per compute minute, so the cost of a run follows the number of businesses you receive. Setting `scrape_contacts` to false turns this Actor into a plain Maps place extractor without contact-enrichment events.

### Pricing

The FREE-tier event prices are $0.0035 per delivered place, $0.0028 per contact-enriched place and $0.0007 per delivered place when a rating, review, website, category or name filter is enabled. A contact-enriched place has at least one email address or social profile found on its website. Merely attempting a website request does not create an enrichment event. Selecting only `skip_closed_places` does not activate the filter event.

Paid Apify tiers use their corresponding event prices displayed in the pricing panel. Apify also applies the Actor start event, with a FREE-tier price of $0.0002 per GB. An empty result produces no place, enrichment or filter events. The current Apify pricing panel is the authoritative listing of applicable prices and platform charges.

Free-plan runs deliver at most ten places. Only those delivered places contribute to the place, contact and filter event counts. Paid users can request the full result limit per query. Review the input and the pricing panel before running a large list of search phrases.

### FAQ

#### Why do some places have no email address?

Google usually supplies a website link rather than an email address. The Actor checks that website for public contact information. A website may omit email addresses, show only a contact form or be unavailable during the run. Social profiles can still be present even when `emails` is empty.

#### Why did I get fewer results than requested?

The result limit is a ceiling. The search area, Google's available place list and your filters determine how many rows remain. Inspect the composed `search_query`, the filter settings and the summary counters before expanding the search.

#### Can I collect Maps places without contact enrichment?

Yes. Set `scrape_contacts` to false. You retain the Maps fields and avoid contact-enrichment events. This is useful for comparing locations, ratings, categories or existing website links.

#### Does a website failure remove the business from my dataset?

No. The Maps row remains available. `website_crawled` is false when the website could not be fetched, and contact fields stay empty. The Actor retries an unreachable website before giving up. If a website still cannot be checked, the run summary lists it in `warnings`, for example `Website could not complete contact enrichment: https://easytimes.amsterdam/`, and sets `partial` to true. A website phase limited by the run budget produces a warning as well. You are not charged a contact-enrichment event for a website that yielded no contacts.

#### What if a business only lists an Instagram or Facebook page?

Then that profile URL is returned in `website`, exactly as Google Maps shows it. The Actor does not crawl social profiles as if they were business websites, so `website_crawled` is false, the contact fields stay empty and no contact enrichment is charged for that place. Profiles that a business links from its own website do appear in `facebook`, `instagram` and the other social fields.

This excerpt from the prefill run (`coffee shops amsterdam`) shows such a place:

```json
{
  "name": "Coffeeshop Bluebird",
  "place_id": "ChIJaRaLkL4JxkcR918LcqGLDwM",
  "rating": 4.5,
  "reviews_count": 3206,
  "website": "https://www.instagram.com/bluebirdcoffeeshopams/",
  "website_domain": "instagram.com",
  "emails": [],
  "website_crawled": false,
  "facebook": null,
  "instagram": null
}
```

#### How do I search several cities?

Put a separate phrase for each city in `queries`, or create separate runs with the location fields. A location field applies to every query in that run. Using explicit category-and-city phrases makes each returned `search_query` easy to inspect.

#### Is this Google Maps email extractor free?

Free-plan users can run it and receive up to ten places per run. Every delivered place is charged at the event prices listed above, also on the free plan, and empty results cost nothing apart from the Actor start event.

#### Which countries and languages are supported?

Any country where Google Maps lists businesses. Set `gl` to the country code for the search region and `hl` for the language of categories and opening hours; the Dutch example above uses `gl` and `hl` set to `nl`.

#### Can I use it through the API or on a schedule?

Yes. Start runs through the Apify API, a schedule, or an integration such as Zapier or Make, and read the dataset as JSON, CSV or Excel. The input is the same as in the Console.

# Actor input Schema

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

Google Maps search queries (e.g. 'restaurants Amsterdam', 'plumbers in NYC').

## `max_results_per_query` (type: `integer`):

Cap places per query (1-400). A single request returns Google's full inventory for the area — typically up to ~300 places — far beyond Google Maps' 120-per-screen web limit.

## `hl` (type: `string`):

Language code (en, nl, de, es…).

## `gl` (type: `string`):

Two-letter country code (us, gb, nl, de…).

## `use_cookies` (type: `boolean`):

Recommended. Bypasses consent banner and reduces CAPTCHA at scale.

## `use_proxy` (type: `boolean`):

Optional. Adds extra IP rotation for very high volume.

## `scrape_contacts` (type: `boolean`):

Crawl each place's website (homepage + contact page) for email addresses, extra phone numbers and social media profiles (Facebook, Instagram, LinkedIn, X, YouTube, TikTok, Pinterest).

## `location_query` (type: `string`):

A place name to search in, such as 'Amsterdam' or 'Greater London'. Maps has no separate location parameter on this endpoint, so this is composed into the search string and the composed query is reported on every row.

## `city` (type: `string`):

Composed into the search string alongside the query.

## `state` (type: `string`):

Composed into the search string alongside the query.

## `postal_code` (type: `string`):

Composed into the search string alongside the query.

## `country_code` (type: `string`):

Composed into the search string. This is the place you are searching, which is not the same as the `gl` result region.

## `minimum_stars` (type: `string`):

Drop places rated below this. An unrated place cannot clear a minimum, so it is dropped too.

## `minimum_reviews` (type: `integer`):

Drop places with fewer reviews than this. Useful for skipping brand-new or barely-visited listings.

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

Places without a website are the classic agency lead list. Places with one are the classic competitor list.

## `skip_closed_places` (type: `boolean`):

Drop places Google marks permanently closed. Temporarily closed places are kept, because a temporarily closed business is still a lead.

## `category_filter_words` (type: `array`):

Keep only places whose Google category matches one of these words. Matched against every category on the place, not just the primary one.

## `search_matching` (type: `string`):

Maps returns loosely related places. This narrows on the place name.

## `cursor` (type: `string`):

Continuation token returned by a previous API page. Keep the other input fields unchanged.

## Actor input object example

```json
{
  "queries": [
    "restaurants Amsterdam",
    "coffee shops Berlin"
  ],
  "max_results_per_query": 20,
  "hl": "en",
  "gl": "us",
  "use_cookies": true,
  "use_proxy": false,
  "scrape_contacts": true,
  "location_query": "Amsterdam",
  "city": "Rotterdam",
  "state": "Noord-Holland",
  "postal_code": "1012",
  "country_code": "NL",
  "minimum_stars": "",
  "website": "any",
  "skip_closed_places": false,
  "category_filter_words": [
    "dentist",
    "orthodontist"
  ],
  "search_matching": "all"
}
```

# Actor output Schema

## `results` (type: `string`):

Dataset rows produced by this run.

## `output` (type: `string`):

OUTPUT record with the run's counts and status flags.

## `errors` (type: `string`):

Failures with a code and a redacted message. Absent when the run had none.

# 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": [
        "coffee shops amsterdam"
    ],
    "max_results_per_query": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("s-r/google-maps-contact-details").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": ["coffee shops amsterdam"],
    "max_results_per_query": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("s-r/google-maps-contact-details").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": [
    "coffee shops amsterdam"
  ],
  "max_results_per_query": 20
}' |
apify call s-r/google-maps-contact-details --silent --output-dataset

```

## MCP server setup

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

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/0SFLYoYX98LiplYs8/builds/EmecScm8X47DyAiyc/openapi.json
