How to use Google Maps Places Scraper
Step by step: in the Apify Store, over the API, from JavaScript or Python, in n8n, and from AI agents through MCP or LangChain.
$2.00 per 1,000 places on the Free plan, down to $1.00 on higher plans (live Apify price on 2026-11-03).
1. In the Apify Store (no code)
- Open arcadium/google-maps-places and sign in to Apify (a free account works).
- Fill in the input form. The fields below are the ones the example uses.
- Set a maximum cost per run in the run options if you want a hard cap.
- Start the run. Rows appear in the Output tab as they arrive.
- Export the dataset as JSON, CSV or Excel, or connect an integration.
| Input field | Type | Description |
|---|---|---|
searchTerms | array | What to look for on Google Maps, one term per line, such as "dentist", "coffee shop" or "plumber". Each term is searched in the area below and returns up to the max places per search. Also accepts searchStringsArray. |
location | string | Where to search: a city, district, region or postcode as you would type it into Google Maps, such as "Austin, TX" or "Schwabing, Munich". The area is the map Google shows for it, searched cell by cell so results are not capped at one result list. Leave empty when you give coordinates or a polygon below, or to read Google's own result list for the term (up to about 200 places). Also accepts locationQuery. |
maxPlacesPerSearch | integer | Places to return for each search term (or search link), 0 = all places found in the area (up to 20,000). Default 50. Each place is one charged result. Also accepts maxCrawledPlacesPerSearch; a compass input without it means all places, as in compass. |
language | string | Language of names, categories and opening hours as a code such as en, de or pt-BR. |
country | string | Google market as a 2-letter code such as us, de or gb (affects ranking, not the area). Also accepts countryCode. |
website | string | Filter by whether the place lists a website. |
skipClosed | boolean | Leave out places Google marks as closed. Also accepts skipClosedPlaces. |
nameMatch | string | Keep only places whose name contains (or equals) the search term, ignoring case and accents. Also accepts searchMatching (all, only_includes, only_exact). |
includeDetails | boolean | Read each place's detail page too: plus code, long description, amenity and accessibility attributes, review topics. One extra request and one extra place-details charge per place. Also accepts scrapePlaceDetailPage. |
includePhones | boolean | Add the phone number the business publishes on its Google listing (on by default). Switch off to leave phone and phoneE164 empty. |
minCellKm | number | Advanced. The area is split into smaller map cells while a cell still returns 100+ places; cells are not split below this size. Default 0.4. |
Example input:
{
"searchTerms": [
"dentist"
],
"location": "Austin, TX",
"maxPlacesPerSearch": 50,
"language": "en",
"country": "us",
"website": "any",
"skipClosed": false,
"nameMatch": "all",
"includeDetails": false,
"includePhones": true,
"minCellKm": 0.4
}
2. Over the Apify API
The synchronous endpoint runs the actor and returns its rows in one call (for runs up to about five minutes; use the asynchronous run endpoint for longer ones).
curl -X POST "https://api.apify.com/v2/acts/arcadium~google-maps-places/run-sync-get-dataset-items" \
-H "Authorization: Bearer $APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"searchTerms":["dentist"],"location":"Austin, TX","maxPlacesPerSearch":50,"language":"en","country":"us","website":"any","skipClosed":false,"nameMatch":"all","includeDetails":false,"includePhones":true,"minCellKm":0.4}'
3. From JavaScript
// npm install apify-client
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('arcadium/google-maps-places').call({
"searchTerms": [
"dentist"
],
"location": "Austin, TX",
"maxPlacesPerSearch": 50,
"language": "en",
"country": "us",
"website": "any",
"skipClosed": false,
"nameMatch": "all",
"includeDetails": false,
"includePhones": true,
"minCellKm": 0.4
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items);
4. From Python
# pip install apify-client
import os
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("arcadium/google-maps-places").call(run_input={
"searchTerms": ["dentist"],
"location": "Austin, TX",
"maxPlacesPerSearch": 50,
"language": "en",
"country": "us",
"website": "any",
"skipClosed": False,
"nameMatch": "all",
"includeDetails": False,
"includePhones": True,
"minCellKm": 0.4,
})
items = client.dataset(run.default_dataset_id).list_items().items # apify-client 3.x returns a Run object
print(items)
5. In n8n
Paste this node into a workflow (it is a plain HTTP Request node). Create a Header Auth credential with name Authorization and value Bearer <APIFY_TOKEN>; each returned row becomes one item for the next node.
{
"nodes": [
{
"name": "Run arcadium/google-maps-places",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4,
"position": [
0,
0
],
"parameters": {
"method": "POST",
"url": "https://api.apify.com/v2/acts/arcadium~google-maps-places/run-sync-get-dataset-items",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "{\"searchTerms\":[\"dentist\"],\"location\":\"Austin, TX\",\"maxPlacesPerSearch\":50,\"language\":\"en\",\"country\":\"us\",\"website\":\"any\",\"skipClosed\":false,\"nameMatch\":\"all\",\"includeDetails\":false,\"includePhones\":true,\"minCellKm\":0.4}",
"options": {
"timeout": 300000
}
}
}
],
"connections": {}
}
Ready-made workflow to import:
- Weekly Google Maps local business leads to Google Sheets Businesses of one type in an area with phone, website, rating and review count; every Monday only the places not in the sheet yet.
6. From an AI assistant (MCP)
Add Apify’s MCP server to Claude Desktop, Cursor, VS Code or any MCP client, limited to this actor:
{
"mcpServers": {
"apify": {
"url": "https://mcp.apify.com/?tools=arcadium/google-maps-places",
"headers": {
"Authorization": "Bearer <APIFY_TOKEN>"
}
}
}
}
More on the MCP page.
7. From LangChain
# pip install langchain-apify (reads APIFY_API_TOKEN from the environment)
from langchain_apify import ApifyActorsTool
tool = ApifyActorsTool("arcadium/google-maps-places")
rows = tool.invoke({"run_input": {
"searchTerms": ["dentist"],
"location": "Austin, TX",
"maxPlacesPerSearch": 50,
"language": "en",
"country": "us",
"website": "any",
"skipClosed": False,
"nameMatch": "all",
"includeDetails": False,
"includePhones": True,
"minCellKm": 0.4,
}})
# or give `tool` to an agent: create_react_agent(llm, [tool])
Sample output
One row, from the example in the actor’s README. Long text is shortened and lists are cut to two items.
{
"rowType": "place",
"placeId": "ChIJCar0f49ZwokR6ozLV-dHNTE",
"cid": "3545819340560108778",
"name": "Katz's Delicatessen",
"personNamed": false,
"category": "Deli",
"address": "205 E Houston St, New York, NY 10002",
"city": "New York",
"postalCode": "10002",
"latitude": 40.722233,
"longitude": -73.987429,
"rating": 4.5,
"reviewCount": 56440,
"website": "https://katzsdelicatessen.com/",
"phone": null,
"phoneE164": null,
"openingHours": [
{
"day": "Monday",
"date": "2026-11-02",
"hours": [
"8 AM–11 PM"
]
}
],
"closed": false,
"searchTerm": null,
"scrapedAt": "2026-11-02T18:00:00.000Z"
}
| Field | What it holds |
|---|---|
rowType | Kind of row. Filter on this first. 'place': one business (charged). 'coverage': one free row per search with places delivered, map cells searched and why it stopped. 'error': an input could not be read (free). |
placeId | Google place id (ChIJ...). Stable; use it to deduplicate between runs and with the Google Places API. |
cid | Google CID of the place (decimal), the number in maps.google.com/?cid=... |
featureId | Google Maps feature id '0x...:0x...' (as in place links). |
placeUrl | Google Maps link of the place. |
name | Business name as Google Maps shows it. |
personNamed | Information only: true when the listing is named after a person (e.g. a sole practitioner's practice, 'Bo Stanley, DDS'). Such a name and phone may be personal data under GDPR; nothing is withheld. |
category | Main Google category ('Dentist'). |
categories | All Google categories of the place. |
description | Google's one-line summary of the place, when it has one. |
address | Full address as Google prints it. |
neighborhood | Neighbourhood or district. |
street | Street and number. |
city | City or town of the address. |
postalCode | Postal code. |
state | State or region. |
countryCode | Country, ISO 3166 alpha-2. |
latitude | Latitude (WGS84). |
longitude | Longitude (WGS84). |
timeZone | IANA time zone ('America/Chicago'). |
website | Website the business lists on Google. |
phone | Business phone as printed in its country, as published on the Google listing; null when includePhones is off or the listing has none. |
phoneE164 | Business phone in E.164 (+15125550100); same rule as phone. |
rating | Google star rating, 1-5. |
reviewCount | Number of Google reviews. |
openingHours | Opening hours of the next 7 days: [{day, date, hours: ['8 AM–5 PM']}] (holiday-aware, dated). |
openNowText | Open-now text at the time of the run ('Open · Closes 5 PM'). |
closed | True when Google marks the place closed (permanently or temporarily). |
bookingLinks | Booking, reservation and ordering links (partner sites), tracking parameters removed. |
details | With includeDetails: {plusCode, longDescription, attributes: [{group, name, value}], reviewTopics: [{name, mentions}]}; null otherwise. |
searchTerm | Search term that found the place (null for places given directly). |
searchLocation | Location or area of that search. |
rank | Position in the delivery order of its search (1-based). |
language | Language of the run. |
scrapedAt | When the row was read (ISO 8601). |
code | Error rows: error code (no_match, not_found, blocked, unavailable, invalid_input). |
message | Error rows: what went wrong. |
delivered | Coverage rows: places delivered for the search. |
cellsSearched | Coverage rows: map cells searched. |
cellsSaturatedAtMin | Coverage rows: cells that still had 100+ places at the smallest cell size (some places there may be missing). |
stopReason | Coverage rows: why the search ended (limit = max places reached, end = area exhausted, budget = run charge limit, error). |
stated | Coverage rows: always null (Google states no total for a map search). |
shortfall | Coverage rows: true when the search ended on an error or some map cells were still full at the smallest cell size. |
shortfallReason | Coverage rows: 'error' or 'below_stated' (cells still full at the smallest size); null without a shortfall. |
Tutorials
Schedule it
In Apify Console, create a Task from your input and add a Schedule (for example daily). Each run writes a new dataset; integrations can push the rows to Google Sheets, Slack, a webhook or your database.