# Google Maps Scraper API — Places, Leads & AI Verdicts (`sherwood/google-maps-scraper`) Actor

Scrape Google Maps places in bulk, in the fields of the most used Google Maps scraper: name, category, address, phone, website, rating, reviews, hours, booking link. Optional AI verdicts per place: digital maturity, chain or independent, size, price tier, ideal-customer fit. $0.50 per 1,000 places.

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

## Pricing

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

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 Scraper API — places, leads and AI verdicts, $0.50 per 1,000 places

Search Google Maps for any term near any place and get every result as a dataset item: name, category, address, phone, website, rating, reviews count, photos count, opening hours, table and appointment booking links, attributes and hotel amenities, listing claimed or not, coordinates, Google IDs (`kgmid`, `businessProfileId`), state or region, a photo, and for hotels the booking offers with Google's price and dates. The fields are named like the most used Google Maps scraper on Apify (`title`, `categoryName`, `totalScore`, `reviewsCount`, `phone`, `claimThisBusiness`...), so a workflow built for it keeps working when you switch the Actor ID. **$0.50 per 1,000 places.**

Every place also comes with **verdicts**. Some are computed from the listing, for free; the others are judged by an AI model when you ask for them, for $1 per 1,000 places:

| Verdict | What it tells you | How |
|---|---|---|
| `reputation` | strong, good, fragile (low rating with many reviews), invisible (fewer than 10 reviews), no_reviews, unknown (see Limits) | computed, free |
| `listingGaps` | unclaimed listing, no opening hours, few photos, no website, no phone | computed, free |
| `bookingTool` | Planity, Treatwell, Fresha, Booksy, Doctolib, TheFork, Zenchef, OpenTable, SevenRooms, Resy and more, own system (for a hotel: its official site among Google's offers), other platform (Google's own booking page, travel sites), none | computed, free |
| `sameWebsiteInResults`, `samePhoneInResults` | how many results share the website or phone: a network, a chain, a lead-generation front | computed, free |
| `intermediarySignals` | keywords in the name, open 24/7, no street address, few reviews, many trades | computed, free |
| `webPresence` | own site, chain site, platform page, free site builder, social page only, none | computed, AI when needed |
| `digitalMaturity` | 0 invisible, 1 minimum, 2 equipped, 3 advanced | AI |
| `bookingExpected`, `bookingMissing` | the business type usually takes bookings, and neither its Google listing nor its homepage shows an online booking tool (`null` when the homepage has a booking button with no known tool, or cannot be read) | AI + computed |
| `ownership` | independent, chain or franchise, local group, public or non-profit | AI |
| `sizeHint` | solo, 2-9, 10-49, 50+ people | AI |
| `priceTier` | budget, mid, premium, luxury | AI |
| `clientele` | consumers, businesses, both | AI |
| `intermediaryProbability` | how much the listing looks like a call platform rather than a local business | AI |
| `idealCustomerFit` | probability that the place matches your ideal customer, described in one sentence | AI |
| `custom` | the answers to up to 5 questions of your own | AI |

Each AI judgment comes with its confidence; when the confidence is low the answer is `unsure`, never a guess. The AI model (Jev, by TypeSafe AI) only sees what the listing shows.

### What you get

```json
{
  "searchString": "restaurant",
  "rank": 1,
  "title": "ART Restaurant Lyon",
  "categoryName": "French restaurant",
  "categories": ["French restaurant", "Brunch restaurant", "Cocktail bar"],
  "address": "3 Rue Sala, 69002 Lyon",
  "street": "3 Rue Sala", "city": "Lyon", "postalCode": "69002", "countryCode": "FR",
  "website": "https://www.art-restaurant.fr/",
  "phone": "+33 9 51 54 79 68",
  "totalScore": 4.8,
  "reviewsCount": 1482,
  "imagesCount": 636,
  "claimThisBusiness": false,
  "reserveTableUrl": "https://bookings.zenchef.com/results?rid=...",
  "bookingLinks": [{"name": "zenchef.com", "url": "https://bookings.zenchef.com/results?rid=..."}],
  "openingHours": [{"day": "Tuesday", "hours": "7 to 9 PM"}],
  "location": {"lat": 45.7568, "lng": 4.8274},
  "placeId": "ChIJOz5bdf3r9EcR6Lc45kXa5As",
  "url": "https://www.google.com/maps/place/?q=place_id:ChIJOz5bdf3r9EcR6Lc45kXa5As",
  "verdicts": {
    "reputation": "strong", "listingGaps": [], "bookingTool": "zenchef", "webPresence": "own_site",
    "digitalMaturityLabel": "advanced", "ownership": "independent", "sizeHint": "10-49", "priceTier": "premium",
    "bookingMissing": false, "intermediaryProbability": 0.04, "idealCustomerFit": 0.9
  }
}
```

The **Leads** view of the dataset shows the AI verdicts side by side, sorted as you like.

### Use cases

- Web and marketing agencies: businesses with no website, only a social page, or an unclaimed listing.
- Booking and SaaS vendors: businesses that usually take bookings but have no online booking, or use a competitor.
- B2B sales: independents versus chains, size and price tier, before calling.
- Lead buyers and directories: listings that look like call platforms rather than local businesses.
- Anyone with a target: describe your ideal customer in one sentence and sort the list by `idealCustomerFit`.

### Emails and social profiles (option)

Set `scrapeContacts: true` and the Actor reads each place's own website: the homepage, then up to two pages of the same site that hold contacts (contact page, legal notice, Impressum). It adds the fields compass uses, so the same workflows keep working:

`emails`, `phones` (from phone links), `facebooks`, `instagrams`, `linkedIns`, `twitters`, `youtubes`, `tiktoks`, `pinterests`, `discords`

- Emails hidden by Cloudflare's protection or written as `name [at] domain [dot] com` are decoded. Placeholders (`name@example.com`) and image names (`logo@2x.png`) are left out, and the business's own domain comes first.
- An email on another domain than the site (and not a free mailbox) is judged by an AI model from the sentence where it appears: the business's own or its group's stays in `emails`; a third party's (the web agency that built the site, a vendor, a host, a consumer mediator) moves to `thirdPartyEmails`.
- Share buttons (`facebook.com/sharer`, `twitter.com/intent`) are not profiles and are left out.
- A social page given as the website goes straight to its field. A booking-platform page (Planity, TheFork...) is not read: its contacts would be the platform's.
- `contactsStatus` says what was done: `ok`, `no_website`, `social_page_only`, `booking_platform_page`, `website_unreadable`. An unreadable website comes with `websiteReadError`: `slow` (no full page within 15 seconds), `refused` (the site turned our IPs away), `error` or `not_html`.
- **$0.001 per place whose website gave at least one email or social profile** (= $1 per 1,000). A website with none is free.

Only what the business publishes on its own website is collected. Emails can be personal data (GDPR and similar laws): use them for a legitimate purpose and follow the rules on direct marketing in your country.

### Qualify a dataset you already have

Already scraped Google Maps with another Actor? Give its dataset in `datasetId` with your `idealCustomer` or `customQuestions`: the places are qualified without scraping again, and only the AI verdicts are charged.

```json
{ "datasetId": "YOUR_GOOGLE_MAPS_DATASET_ID", "idealCustomer": "independent restaurant without online booking" }
```

### Input

| Field | Type | Default | Notes |
|---|---|---|---|
| `searchStringsArray` | array | `["restaurant"]` | Search terms. Aliases: `query`, `queries`, `search`, `searchTerms`, `keywords`. |
| `locationQuery` | string | — | Where to search. Alias: `location`. Leave empty when the terms already name the place ("dentist in Bordeaux"). |
| `maxCrawledPlacesPerSearch` | integer | `50` | Up to 120 per term (Google's limit for one search). |
| `language` | string | `en` | Language of the results. |
| `maxPlaces` | integer | `1000` | Unique places per run. |
| `qualifyPlaces` | boolean | `false` | Add the AI verdicts. |
| `idealCustomer` | string | — | One sentence; adds `idealCustomerFit`. |
| `customQuestions` | array | — | Up to 5: `{"id": "terrace", "question": "Outdoor terrace?"}`, with `options` for a choice. |
| `scrapeContacts` | boolean | `false` | Emails and social profiles from each place's website. |
| `datasetId` | string | — | Qualify an existing dataset instead of searching. |

### Pricing

- **Place scraped** — $0.0005 per unique place (= $0.50 per 1,000), computed verdicts included.
- **Place qualified** — $0.001 per place judged by the AI model (= $1 per 1,000).
- **Contacts found** — $0.001 per place whose website gave at least one email or social profile, with `scrapeContacts` (= $1 per 1,000).
- Duplicates and places beyond your run budget: free.

### Via API, n8n, Make or MCP

`POST https://api.apify.com/v2/actors/sherwood~google-maps-scraper/run-sync-get-dataset-items` with the JSON input above, or add it as a tool through the Apify MCP server.

### Limits

- Google returns about 120 places per search: split a large area into several terms (one per district or town).
- Google sometimes serves a reduced results page, without review and photo counts, depending on the IP. The Actor asks again from up to 10 other IPs; if every answer is reduced, the places come with `reducedGooglePage: true`, `reviewsCount: null` and a reputation of `unknown`, and the run's status message says how many.
- Not included, because only the place detail page shows them: reviews distribution by stars, the owner's photo gallery, popular times, Q\&A.
- Reviews are not included: Google Maps no longer shows them to signed-out visitors. To analyze reviews, chain a reviews scraper with [Review Analyzer API](https://apify.com/sherwood/review-analyzer).
- AI verdicts are judgments from the listing alone; check before acting on an important one.

# Actor input Schema

## `searchStringsArray` (type: `array`):

What to search on Google Maps, one term per entry ("dentist", "plumber", "vegan restaurant"). Each term is searched near locationQuery. Aliases: query, queries, search, searchTerms, keywords (a string with commas or new lines is split).

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

City, region or address to search around ("Lyon, France", "Brooklyn, NY"). Alias: location. Leave it empty when the search terms already name the place ("dentist in Bordeaux").

## `maxCrawledPlacesPerSearch` (type: `integer`):

Places kept per search term, in Google's order. Google stops around 120 results per search; for more, split the area (one term per district or nearby town).

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

Language of the Google Maps results (category names, opening hours).

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

Stop after this many unique places over all search terms. Default 1,000.

## `scrapeContacts` (type: `boolean`):

Read each place's own website (homepage, then up to two pages such as the contact page, legal notice or Impressum) for emails, phone links and social profiles (Facebook, Instagram, LinkedIn, X, YouTube, TikTok, Pinterest, Discord). Emails hidden by Cloudflare or written as name \[at] domain are decoded. $0.001 per place whose website gave at least one email or social profile; a website with none is free.

## `qualifyPlaces` (type: `boolean`):

Judge each place with an AI model from its listing: digital maturity (0-3), online booking expected, independent / chain / local group, size, price tier, clientele, and how much the listing looks like a call platform. $0.001 per place qualified.

## `idealCustomer` (type: `string`):

Each place gets idealCustomerFit, a probability from 0 to 1 that it matches this description, to sort the list. Example: "independent high-end restaurant that takes online reservations". Turns the AI verdicts on.

## `customQuestions` (type: `array`):

Questions asked about every place from its listing. Without options: yes/no, answered with a probability. With options: one of them. With "type": "scale": one level of the options, in order. Turns the AI verdicts on.

## `datasetId` (type: `string`):

Add the verdicts to the places of an Apify dataset, for example a compass/crawler-google-places run, without scraping again. Only place-qualified is charged.

## `query` (type: `string`):

Alias of searchStringsArray: one term, or several separated by commas.

## `location` (type: `string`):

Alias of locationQuery.

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

Apify Proxy is used by default. Residential proxies are rarely needed.

## Actor input object example

```json
{
  "searchStringsArray": [
    "restaurant"
  ],
  "locationQuery": "Lyon, France",
  "maxCrawledPlacesPerSearch": 20,
  "language": "en",
  "maxPlaces": 1000,
  "scrapeContacts": false,
  "qualifyPlaces": false,
  "customQuestions": [
    {
      "id": "terrace",
      "question": "Does the listing suggest an outdoor terrace?"
    },
    {
      "id": "cuisine",
      "question": "What is the main cuisine?",
      "options": [
        "French",
        "Italian",
        "Asian",
        "Other"
      ]
    }
  ],
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

One item per unique place, in compass/crawler-google-places field names, with verdicts.

# 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 = {
    "searchStringsArray": [
        "restaurant"
    ],
    "locationQuery": "Lyon, France",
    "maxCrawledPlacesPerSearch": 20,
    "language": "en",
    "customQuestions": [
        {
            "id": "terrace",
            "question": "Does the listing suggest an outdoor terrace?"
        },
        {
            "id": "cuisine",
            "question": "What is the main cuisine?",
            "options": [
                "French",
                "Italian",
                "Asian",
                "Other"
            ]
        }
    ],
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("sherwood/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 = {
    "searchStringsArray": ["restaurant"],
    "locationQuery": "Lyon, France",
    "maxCrawledPlacesPerSearch": 20,
    "language": "en",
    "customQuestions": [
        {
            "id": "terrace",
            "question": "Does the listing suggest an outdoor terrace?",
        },
        {
            "id": "cuisine",
            "question": "What is the main cuisine?",
            "options": [
                "French",
                "Italian",
                "Asian",
                "Other",
            ],
        },
    ],
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("sherwood/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 '{
  "searchStringsArray": [
    "restaurant"
  ],
  "locationQuery": "Lyon, France",
  "maxCrawledPlacesPerSearch": 20,
  "language": "en",
  "customQuestions": [
    {
      "id": "terrace",
      "question": "Does the listing suggest an outdoor terrace?"
    },
    {
      "id": "cuisine",
      "question": "What is the main cuisine?",
      "options": [
        "French",
        "Italian",
        "Asian",
        "Other"
      ]
    }
  ],
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call sherwood/google-maps-scraper --silent --output-dataset

```

## MCP server setup

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