Google Maps Places Scraper
Give it a search term and an area (a city, district or postcode, a bounding box, a circle or a GeoJSON polygon) and it returns every business Google Maps shows there as one JSON row each: name, categories, full address and its parts, coordinates, star rating and review count, website, phone, opening hours for the next 7 days, open-now text and booking links. Or give it place links, place IDs or CIDs. $2.00 per 1,000 places; you pay only for rows you get.
$2.00 per 1,000 places on the Free plan, down to $1.00 on higher plans
Why this actor
- Past one result list. One Google Maps search stops at about 200 places. The area is split into map cells and every cell that is still full is split again, so a city returns all of its dentists, not the first page.
- Fast and light. Google's own data answers, no browser.
- Listings as Google shows them. Name, phone and website exactly as the business publishes them on its Google listing. No reviewer names or profiles, nothing behind a login; emails and phone numbers inside free-text descriptions are removed.
- Know what you got. One free
coveragerow per search: places delivered, map cells searched, cells that were still full at the smallest size, places filtered out, and why the search stopped. A freeerrorrow with acodefor every input that could not be read. - Paste your input. compass/crawler-google-places inputs run as they are for places:
searchStringsArray,locationQuery(orcity/state/county/postalCode/countryCode),maxCrawledPlacesPerSearch(missing = all places, as in compass),language,placeMinimumStars,categoryFilterWords,searchMatching,website,skipClosedPlaces,scrapePlaceDetailPage,customGeolocation(Polygon, MultiPolygon, FeatureCollection of polygons, Point withradiusKm),startUrlsandplaceIds(place links, place ids, search links). Reviews, images, contacts, leads, social profiles, table reservations, order-online, web results and directories are not part of this actor: those fields are ignored with a warning. Inputs of scraperlink/google-maps-scraper (query,location,num,hl,gl, coordinates) and vortex_data/google-maps (locationQueries,languages,extractPlaceDetails) also run; several locations use the first.
What you get
Place row (rowType: "place", charged as place):
| Field | What it holds |
|---|---|
placeId, cid, featureId, placeUrl | Google's ids of the place and its Maps link. placeId is stable: deduplicate on it between runs. |
name, personNamed | Business name as Google shows it; personNamed: true when the listing is named after a person (e.g. a sole practitioner's practice, "Bo Stanley, DDS"). |
category, categories, description | Main and all Google categories; Google's one-line summary. |
address, street, neighborhood, city, postalCode, state, countryCode | Full address and its parts. |
latitude, longitude, timeZone | Location. |
rating, reviewCount | Google stars and number of reviews. |
website, bookingLinks | Website the business lists; booking and ordering links (tracking removed). |
openingHours, openNowText, closed | Hours of the next 7 days ([{ day, date, hours }], holiday-aware), open-now text, closed flag. |
phone, phoneE164 | Business phone as published on the listing (on by default; switch includePhones off to leave them empty). |
details | With includeDetails (charged as place-details): plus code, long description, amenity and accessibility attributes, review topics with mention counts. null (and not charged) when the detail request failed. |
searchTerm, searchLocation, rank, scrapedAt | Which search found it, its position, when it was read. |
Coverage row (rowType: "coverage", free): delivered, duplicates (places an earlier search of the run already delivered), cellsSearched, cellsSplit, cellsSaturatedAtMin (cells still full at the smallest size: places there may be missing; lower minCellKm to go deeper), filteredOut, outsideArea, personNamed, detailsFailed, detailsSkipped (places delivered without a detail request while details were paused after 5 failures in a row; one retry after 25 places turns them back on), detailsCapped, pages and maxPages, areaSource (location, coordinates, link), stopReason (limit: max places reached; end: the area is exhausted; budget: the page budget or the run's max charge was reached; error), capSource (what cut the search: user for a limit you set or the run's max charge, default for a default page budget, site when Google's own result cap left places out).
Page budget. Filters run on the places Google returns, so a strict filter over a big area could read a whole city and keep almost nothing. Each search therefore stops after maxPagesPerSearch search pages (default 3 per 20 places asked for, at least 30; "all places" = 3,000 pages) and the run after maxPagesPerRun (default 5,000), with stopReason: "budget".
Error row (rowType: "error", free): input, code (invalid_input, not_found, no_match, blocked, unavailable), message. Retry blocked and unavailable later.
Listings are published by the businesses for customer contact. A sole trader's name and phone may still be personal data under GDPR; you are responsible for using them lawfully.
Reviews of the places are not part of this actor: pass the placeId values to our Google Maps Reviews Scraper.
Sample row (from the README example; long text shortened, lists 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"
}
Price
Live Apify price on 2026-11-03, per 1,000 rows, by Apify plan. You pay only for rows delivered; error and summary rows marked free in the README cost nothing. Plus $0.001 per GB of memory when a run starts.
| Per 1,000 | Free | Bronze | Silver | Gold | Platinum | Diamond |
|---|---|---|---|---|---|---|
| Place (main) | $2.00 | $1.60 | $1.40 | $1.00 | $1.00 | $1.00 |
| Place details | $0.50 | $0.50 | $0.50 | $0.50 | $0.50 | $0.50 |
Run it from code or an AI agent
Actor ID arcadium/google-maps-places. Set APIFY_TOKEN to your Apify API token. The input below is the actor’s prefilled example; see the guide for every field.
curl
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}'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);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)MCP
{
"mcpServers": {
"apify": {
"url": "https://mcp.apify.com/?tools=arcadium/google-maps-places",
"headers": {
"Authorization": "Bearer <APIFY_TOKEN>"
}
}
}
}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])LlamaIndex
# pip install llama-index-readers-apify
import os
from llama_index.core import Document, VectorStoreIndex
from llama_index.readers.apify import ApifyActor
reader = ApifyActor(os.environ["APIFY_TOKEN"])
documents = reader.load_data(
actor_id="arcadium/google-maps-places",
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,
},
dataset_mapping_function=lambda item: Document(text=str(item.get("description") or ""), metadata={"url": item.get("url")}),
)
index = VectorStoreIndex.from_documents(documents)CrewAI
# pip install crewai crewai-tools langchain-apify (reads APIFY_API_TOKEN from the environment)
from crewai import Agent
from crewai_tools import ApifyActorsTool
tool = ApifyActorsTool(actor_name="arcadium/google-maps-places")
researcher = Agent(
role="Research analyst",
goal="Answer with data collected by the tool",
backstory="Uses public web data.",
tools=[tool],
)n8n
{
"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": {}
}Google Sheets
// Extensions > Apps Script. Store your token once: Project Settings > Script properties > APIFY_TOKEN.
function importLastRun() {
const token = PropertiesService.getScriptProperties().getProperty('APIFY_TOKEN');
const url = 'https://api.apify.com/v2/acts/arcadium~google-maps-places/runs/last/dataset/items?status=SUCCEEDED&clean=true&format=json';
const res = UrlFetchApp.fetch(url, { headers: { Authorization: 'Bearer ' + token } });
const rows = JSON.parse(res.getContentText());
if (!rows.length) return;
const keys = Object.keys(rows[0]);
const values = rows.map((r) => keys.map((k) => (typeof r[k] === 'object' ? JSON.stringify(r[k]) : r[k])));
const sheet = SpreadsheetApp.getActiveSheet();
sheet.clearContents();
sheet.getRange(1, 1, 1, keys.length).setValues([keys]);
sheet.getRange(2, 1, values.length, keys.length).setValues(values);
}Tutorials
Limitations
- Price level and popular times are not in the data Google serves without signing in; they are not delivered.
closeddoes not tell permanently and temporarily closed apart.- One location per run; several search terms share it. When both a location and coordinates are given, the coordinates are used (with a warning).
- Run time: each place with details is one more request (~0.5 s). A run reads at most 4,000 place details; places past that come without details (not charged for details, counted as
detailsCapped). The default run timeout is 1 hour: rows delivered before a timeout are kept and charged, but the coverage row of the search in progress is not written. For more than ~4,000 places with details, raise the timeout and split the run.