# Google Maps Scraper & Email Extractor – Business Leads (`locomint/google-maps-scraper`) Actor

Google Maps scraper and email extractor for lead generation. Search any category in any city and get name, address, phone, website, rating and weekly hours, plus role emails, social profiles and WhatsApp from each business's own website. Free filters, and you pay only for the places returned.

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

## Pricing

from $2.40 / 1,000 result delivereds

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 Scraper & Email Extractor – Business Leads

This Google Maps scraper and email extractor turns a search term and a city into a list of business records, then visits each business's own website for the email addresses, social profiles and WhatsApp number it publishes. One price covers all of it, and you pay only for the places returned.

```json
{
  "searchStringsArray": ["dentist"],
  "locationQuery": "London, United Kingdom",
  "maxCrawledPlacesPerSearch": 100
}
```

### What it does

For every search term the actor reads the listing results for your location, 50 at a time, until it reaches your cap. A plain map search runs dry at about 100 to 120 results per area, so for bigger asks it splits the area it resolved into a grid of up to 64 cells and searches each one, removing duplicates by `place_id`. The sweep ends early once three cells in a row add fewer than two new places each, because by then the area is exhausted.

Results outside the resolved area are dropped. Deep result pages drift: page four of "restaurant in Lagos, Nigeria" once returned restaurants in Lagos, Portugal. The area check keeps rows like that out of your dataset.

Each place 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. From those pages it takes every email address the business publishes (company mailboxes first), social profile links (Facebook, Instagram, LinkedIn, X, YouTube, TikTok), personal profile links the site publishes for its staff, a WhatsApp number from click-to-chat links, the contact page URL, the technologies the site runs on (matched against about 5,800 technology fingerprints, among them WordPress, Shopify, Wix and HubSpot) and a `website_status`.

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

#### What each row contains

| Group | Columns |
|---|---|
| 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_range`, `price_level` |
| Status and hours | `status`, `open_now`, `hours`, `hours_status` |
| Media and attributes | `photos_count`, `photo_urls`, `attributes` |
| From the website | `emails`, `named_emails`, `socials`, `named_profiles`, `whatsapp`, `website_phones`, `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`.

`emails` holds every address the business publishes on its own website: company mailboxes such as `info@` and `bookings@` first, then addresses that name a member of staff. `named_emails` repeats the named ones, so removing them from `emails` leaves company mailboxes only. `named_profiles` holds personal profile links the site publishes for its staff, such as `linkedin.com/in/maria-silva`, while `socials.linkedin` is always the company's own page. The actor records a profile link and never opens the profile, which sits behind a login.

`hours` is keyed by weekday. Each day is a list of `{"open": "09:00", "close": "18:00"}` periods in local 24-hour time; an empty list means closed that day, `null` means unknown, and `00:00` to `24:00` means open all day. `hours_status` is `ok` when the full week was read, `not_published` when the listing gives no hours, and `unavailable` when the listing has hours but the read did not return the whole week.

### Who it is for

- **Lead generation.** Every dentist, roofer or accountant in a city, with the listing phone number, the practice's own role mailbox and its WhatsApp line in one row. Set `websiteFilter` to `with` to keep only places that have a site to read.
- **Sales prospecting.** Qualify before you call: `rating`, `review_count` and `open_now` show whether a business is active, and `tech_stack` shows what its site runs on, from the CMS to the chat widget.
- **Agencies.** `websiteFilter: "without"` lists businesses with no website at all, and `website_status: "parked"` finds the ones whose domain has lapsed. Both are web-design pitch lists.
- **Local SEO.** Benchmark a client against every competitor in its category with `rating`, `review_count`, `primary_category`, `photos_count` and `attributes` side by side.
- **CRM enrichment.** Paste the place IDs you already hold into `placeIds` and get fresh records without searching. `place_id` does not change between runs, so it is a safe join key.
- **Market research.** Count a category across a city, map it with `lat` and `lng`, and compare rating spreads between districts or between cities.
- **AI agents and RAG.** Set `outputFormat` to `compact-for-ai` and every row comes back trimmed for a context window, with a text line to put straight in a prompt and a citation for the answer. See [Compact rows for AI agents](#compact-rows-for-ai-agents).

### How to use it

1. Open the actor in Apify Console and click **Try for free**.
2. Enter one or more **search terms**, one per line: `dentist`, `coffee shop`, `car rental`.
3. Enter the **location** with the country: `Warsaw, Poland`, not `Warsaw`. In our own tests a bare "Warsaw" resolved to Warsaw, Indiana.
4. Set **max places per search term** (up to 500) and any filters.
5. Click **Start**. Rows appear in the dataset as they are finished; export them to CSV, Excel or JSON, or read them 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-scraper/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"searchStringsArray": ["dentist"], "locationQuery": "London, United Kingdom", "maxCrawledPlacesPerSearch": 50}'
```

That endpoint waits at most 300 seconds, which is about 100 places with details on. For bigger runs 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-scraper").call(run_input={
    "searchStringsArray": ["dentist"],
    "locationQuery": "London, United Kingdom",
    "maxCrawledPlacesPerSearch": 500,
    "websiteFilter": "with",
})
for place in client.dataset(run.default_dataset_id).iterate_items():
    print(place["name"], place["phone_e164"], place["emails"])
```

### Input example

```json
{
  "searchStringsArray": ["dentist", "orthodontist"],
  "locationQuery": "London, United Kingdom",
  "maxCrawledPlacesPerSearch": 200,
  "placeMinimumStars": "4",
  "websiteFilter": "with",
  "skipClosedPlaces": true,
  "categoryFilterWords": ["dentist", "orthodontist", "dental"],
  "searchMatching": "all",
  "includeNearbyPlaces": false,
  "includeDetails": true,
  "includeEnrichment": true,
  "outputFormat": "full",
  "language": "en"
}
```

| Field | Default | What it does |
|---|---|---|
| `searchStringsArray` | – | Search terms, each searched separately. Up to 25 per run; extra terms are ignored with a warning in the log. |
| `locationQuery` | – | City, area or address, with the country. |
| `maxCrawledPlacesPerSearch` | 20 | Cap per search term, 1 to 500. |
| `placeIds` | – | Skip the search and read these places directly: `ChIJ…` place IDs or `0x…:0x…` feature IDs, up to 1,000. If you also give search terms, the IDs are read first. |
| `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). |
| `searchMatching` | `all` | `only_includes`: the name must contain the search term. `only_exact`: the name must equal it. Useful for brand searches. |
| `includeNearbyPlaces` | false | Keep results outside the resolved area. Off by default because they are usually a same-name place elsewhere. |
| `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. |
| `outputFormat` | `full` | `full` is every column. `compact-for-ai` trims each row to the facts a model needs and adds a text line and a citation. Same price either way. |
| `language` | `en` | Language for names and categories (`en`, `de`, `ar`, `ja`, …). |

Give either `searchStringsArray` with `locationQuery`, or `placeIds`.

### Output example

A record from a `restaurant` run 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"],
  "service_area_business": false,
  "phone": "+351 21 342 0607",
  "phone_e164": "+351213420607",
  "website": "https://belcanto.pt/",
  "rating": 4.6,
  "review_count": 1858,
  "status": "open",
  "open_now": true,
  "timezone": "Europe/Lisbon",
  "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"],
  "named_emails": [],
  "socials": {
    "facebook": "https://www.facebook.com/belcantolisboa",
    "instagram": "https://www.instagram.com/belcantolisboa",
    "linkedin": null,
    "x": null,
    "youtube": null,
    "tiktok": null
  },
  "named_profiles": [],
  "whatsapp": null,
  "contact_form_url": "https://belcanto.pt/contact/",
  "tech_stack": ["wordpress", "react", "google-analytics"],
  "website_status": "ok"
}
```

Missing values are `null` or an empty list, never guessed. A business with no website has `website_status: "no_website"` and empty contact columns.

### Compact rows for AI agents

Set `outputFormat` to `compact-for-ai` and each row is written for a context window instead of a spreadsheet: the facts a model needs to answer a "near me" question, the same business as one line of text, and a citation saying which place ID the facts came from, which data path answered and when it was read. The reads and the price are exactly the same as a full run; only the columns change.

Use it when the dataset is going into a retrieval index, a prompt or an agent's tool result. Three things make that work:

- **The `text` column is the prompt.** Forty fields of JSON cost tokens and answer nothing; one line per business costs a fraction of that and models read it more reliably. Concatenate the column and you have the context block.
- **Every row carries its citation.** An answer grounded in local data is only worth its provenance, so `citation.place_id` traces the claim back to the listing and `citation.fetched_at` says how old it is. Opening hours and closures move; a model quoting them should be able to say when they were read.
- **`null` is not an empty list.** `emails: []` means the website was read and publishes no email address. `null` means nothing was read: there is no website, `includeEnrichment` was off, or the site did not answer. Reporting `[]` in that case would state a fact nobody checked, and a model would repeat it. A compact row leaves `website_status` out, so `null` is the only thing left to tell you the difference — use the full format when you need to know which of the three it was.

One row from a `restaurant` run in `Lisbon, Portugal`:

```json
{
  "place_id": "ChIJ…",
  "cid": "11928394857463728190",
  "name": "Belcanto",
  "categories": ["Fine dining restaurant", "Portuguese restaurant"],
  "address": "Largo de São Carlos 10, 1200-410 Lisboa, Portugal",
  "city": "Lisbon",
  "country_code": "PT",
  "phone": "+351213420607",
  "website": "https://belcanto.pt/",
  "rating": 4.6,
  "review_count": 1858,
  "status": "open",
  "open_now": true,
  "hours_today": "12:30-15:00, 19:00-23:00",
  "emails": ["press@belcanto.pt", "work@belcanto.pt"],
  "named_emails": [],
  "named_profiles": [],
  "whatsapp": null,
  "socials": {
    "facebook": "https://www.facebook.com/belcantolisboa",
    "instagram": "https://www.instagram.com/belcantolisboa"
  },
  "contact_form_url": "https://belcanto.pt/contact/",
  "citation": {
    "place_id": "ChIJ…",
    "source": "maps_public",
    "fetched_at": "2026-09-12T09:14:02.918431+00:00"
  },
  "text": "Belcanto | Fine dining restaurant | 4.6/5 (1858 reviews) | Largo de São Carlos 10, 1200-410 Lisboa, Portugal | +351213420607 | https://belcanto.pt/ | today 12:30-15:00, 19:00-23:00 | press@belcanto.pt"
}
```

`hours_today` needs the listing read, so leave `includeDetails` on. Both ids stay on the row: `place_id` and `cid` are the join keys back to your own records, and a spreadsheet column cannot reach inside `citation`. `socials` holds only the business's own pages, as everywhere else; addresses that name a person are listed in `named_emails` and personal profile links in `named_profiles`, so you can strip both before a row goes into a prompt. The `text` line carries the first address in `emails`, which is a company mailbox whenever the site publishes one. The dataset has a **Compact for AI** tab that shows these columns; the other tabs stay for full runs.

### 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 |
|---|---|---|
| Place delivered | $0.003 ($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 in London: 1,000 x $0.003 = $3.00, plus $0.00005 for the start.

What you are not charged for:

- **Filtered-out places.** With search terms, the filters run on the search result before the listing and the website are read. Either way, a place that fails a filter is never written to the dataset.
- **Duplicates.** A place found by two search terms in the same run is delivered once.
- **Empty searches.** A term 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 places does one search term return?**
Up to `maxCrawledPlacesPerSearch`, which goes to 500. A plain map search stops at about 100 to 120 results per area, so past that the actor sweeps a grid over the city. Big categories in big cities reach the cap; a niche category returns what exists.

**Why do some places have no email?**
The business has no website, the site did not answer, or it publishes no email address 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. Many small businesses offer a contact form instead, which `contact_form_url` captures. A named address on another company's domain, such as the web designer credited in the footer, is not returned.

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

**Are opening hours for the whole week?**
Yes, when the listing publishes them and `includeDetails` is on: `hours` carries all seven days and `hours_status` is `ok`. The source sometimes answers with today's hours only; the actor then reads the listing again at once and a third time a few seconds later, and a row that still lacks the full week says `hours_status: "unavailable"` so you know to re-read that place.

**How fresh is the data?**
Every run reads the listings and the websites live, at the time of the run; nothing comes from a stored database. Each row carries `fetched_at` for the listing read and `enriched_at` for the website read.

**Which countries does it cover?**
Any country with map listings. There is no country targeting: the location you type decides where the search runs, so always include the country. The same search code is checked every day in 30 cities across 28 countries.

**Is it legal to scrape Google Maps?**
The actor reads publicly available listing pages and business websites while logged out. It never signs in, never solves a CAPTCHA, and backs off when a site refuses it rather than retrying through another IP. Whether a particular use is lawful depends on your jurisdiction and on what you do with the data, and that part is your responsibility.

**Can I give it place IDs instead of searching?**
Yes. Put up to 1,000 `ChIJ…` or `0x…:0x…` IDs in `placeIds` and the actor reads those places directly. If you have place-page links rather than IDs, the Google Maps Place Details Scraper below accepts those too.

### Limits

- **Speed.** Listing reads are paced at about 20 a minute, so a run with details on delivers roughly 20 places a minute and 1,000 places take close to an hour. With `includeDetails` off, one search call returns up to 50 places and the website visits become the slow part.
- **Run size.** 25 search terms per run, 500 places per term, 1,000 place IDs.
- **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. Each site gets 30 seconds. A homepage that has not answered by then is abandoned: that row keeps its `website` but shows `website_status: "unreachable"` with `enriched_at: null`, meaning the site was not read. The contact page gets at most 10 seconds of what is left, and when it does not answer the row keeps everything the homepage gave. In a compact row the same place has `emails`, `socials`, `whatsapp` and `contact_form_url` set to `null` rather than empty, for the same reason.
- **Emails.** Company mailboxes such as info@, sales@ and bookings@ come first, then addresses that name a member of staff, which are repeated in `named_emails`. A named address is returned when it is on the business's own domain or on a free mail provider such as Gmail (a sole trader's own mailbox, for example), and not when it is on another company's domain. Any other address on another domain 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` (the old `$$` form) has not been populated in any record we have captured - listings now print a band such as "$10-20", which is `price_range`, and even that is not on every read, 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. `street`, `region` and `postal_code` are filled only when the listing's structured address carries them. Australian localities come through with the state attached (`"Melbourne VIC"`).

### Compliance

Contact data comes only from what the business publishes on its own website. That includes addresses that name a member of staff and links to staff profiles, which are kept in `named_emails` and `named_profiles` so you can use them or leave them out; a profile page is never opened. A named work address is personal data in the EU and the UK. You are responsible for using the data lawfully, including whether you may write to a named person (for example under GDPR, CAN-SPAM, PECR and local law).

Questions, bug reports and feature requests go on this actor's Issues tab. Anyone can ask for their own address or profile link to be left out, and a business can ask for its whole site to be left out, by writing to info@locomint.io; that address is for removal requests only, and removals are honoured in every Locomint listing. 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 – Multi-City Lead Lists with Emails](https://apify.com/locomint/google-maps-multi-city-scraper): Many categories across many cities in one deduplicated run.
- [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 – Mailbox-Level SMTP Check](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.
- [Career Site Job Scraper – Greenhouse, Lever, Ashby, Workable](https://apify.com/locomint/career-site-jobs-scraper): Open vacancies straight from an employer's own job feed.
- [Tech Stack Detector – 5,800 Technologies with Versions](https://apify.com/locomint/tech-stack-detector): What a list of websites runs, with versions.
- [Google Maps Email Extractor – Every Address Verified](https://apify.com/locomint/google-maps-email-extractor): Business leads whose email address the mail server confirmed.
- [Google Ads Transparency Scraper – Competitor Ads by Website](https://apify.com/locomint/google-ads-transparency-scraper): The ads a business runs on Search, YouTube, Shopping, Maps and Play.

# Actor input Schema

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

What to look for - one or more categories or business names, e.g. "dentist", "coffee shop", "car rental". Each term is searched separately. Up to 25 per run.

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

City, area or address. Always add the country for unambiguous results: "Warsaw, Poland" rather than "Warsaw".

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

Upper limit of places to collect for each search term. You are charged per place delivered. Large asks sweep the whole city automatically.

## `placeIds` (type: `array`):

Already have a list? Paste place IDs (ChIJ... or 0x...:0x... feature IDs) and get the full record with enrichment for each - no search needed. Up to 1,000 per run.

## `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.

## `searchMatching` (type: `string`):

Restrict results by how the place name relates to the search term. Useful when searching for a brand.

## `includeNearbyPlaces` (type: `boolean`):

By default, results outside the resolved city area are dropped (they are usually a different place with the same name). Turn on to keep them.

## `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 the email addresses it publishes (company mailboxes first; named staff addresses are also listed in named_emails), social profiles, staff profile links (named_profiles), WhatsApp, contact page and tech stack. Included in the price.

## `outputFormat` (type: `string`):

Full record gives every column. Compact for AI trims each row to the facts a model needs, adds a one-line text summary of the business and a citation (place ID, source, time read) so an answer can be traced back. Same reads, same price, fewer columns.

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

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

## Actor input object example

```json
{
  "searchStringsArray": [
    "restaurant"
  ],
  "locationQuery": "Lisbon, Portugal",
  "maxCrawledPlacesPerSearch": 5,
  "placeMinimumStars": "",
  "websiteFilter": "any",
  "skipClosedPlaces": false,
  "searchMatching": "all",
  "includeNearbyPlaces": false,
  "includeDetails": true,
  "includeEnrichment": true,
  "outputFormat": "full",
  "language": "en"
}
```

# Actor output Schema

## `places` (type: `string`):

One record per business: identity, location, contact, reputation, status and website enrichment.

# 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": "Lisbon, Portugal",
    "maxCrawledPlacesPerSearch": 5
};

// Run the Actor and wait for it to finish
const run = await client.actor("locomint/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": "Lisbon, Portugal",
    "maxCrawledPlacesPerSearch": 5,
}

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

```

## MCP server setup

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