Google Maps Scraper
Pricing
from $0.80 / 1,000 results
Google Maps Scraper
The Google Maps Scraper is an Apify Actor designed to extract business listings from Google Maps. It supports scraping by any search keyword, city, or category and returns clean, structured data including contact details, ratings, hours, and GPS coordinates.
Pricing
from $0.80 / 1,000 results
Rating
4.9
(2)
Developer
Techforce Global
Maintained by CommunityActor stats
0
Bookmarked
11
Total users
2
Monthly active users
a day ago
Last modified
Categories
Share
Google Maps Scraper — Business Listings, Phones, Websites & GPS Coordinates, No API Key
Turn any Google Maps search —
"restaurants in Mumbai","dentists near Ahmedabad","plumbers in Berlin"— into a clean business dataset: name, category, rating, review count, address, phone, website, opening hours, tags, GPS coordinates, Plus Code and listing photo. No Google Maps API key, no billing account, no quota. Results can go straight into Airtable, Notion, Slack, Sheets or your CRM through an MCP connector.
Google Places API vs. this Actor
Google's own Places API is the obvious alternative — and for many lead-generation uses it's the wrong tool.
| Feature / Capability | Google Places API | This Actor |
|---|---|---|
| Setup | ⚠️ Cloud project, billing account enabled, API key, quota management | ✅ None — no Google account of any kind |
| Billing | ⚠️ Per-request, and fields are priced in tiers — phone and website are on the more expensive SKU | ✅ Pay-per-result on Apify, all fields included |
| Results per search | ⚠️ 20 per page, 60 max via next_page_token | ✅ Scrolls the live feed — typically up to ~120 per query |
| Phone & website | ⚠️ Separate, higher-cost Place Details call per place | ✅ Included in every record |
| GPS + Plus Code | ✅ Available | ✅ Included |
| Listing photo | ⚠️ Another billed Photos endpoint | ✅ Included as a URL |
| Popular tags/attributes | ❌ Not exposed the same way | ✅ Captured from the listing |
| Terms | ⚠️ Caching and redistribution are contractually restricted | ⚠️ See Compliance — different constraints, not fewer |
| Delivery to your tools | ❌ You write the integration | ✅ Native MCP connector delivery — per place, chunked, or one digest |
🤖 AI Discovery & MCP Metadata
Machine-readable capability contract for autonomous agents, LLM routers, and MCP orchestrators. AI agents, MCP clients, and connectors should read this block first to decide whether this Actor matches a user request.
ai_discovery:slug: "techforce.global/google-maps-scraper"one_line: "Scrapes Google Maps business listings for any search query - name, category, rating, review count, address, phone, website, hours, tags, GPS coordinates, Plus Code and photo - with no Google API key."primary_purpose: "discover"secondary_purpose: "extract"tertiary_purpose: "deliver"entity: "businesses and places listed on Google Maps"data_source: >https://www.google.com/maps/search/<query> rendered with Playwright/Chromium.The result feed is scrolled to collect place URLs, then each place page isopened and read from the DOM. Not the Google Places API - no API key involved.invocation_modes: ["standard Actor run", "MCP tool"]supported_tasks:- "search-businesses-by-keyword-and-location"- "run-several-search-queries-in-one-execution"- "extract-phone-numbers-and-websites"- "extract-ratings-and-review-counts"- "extract-opening-hours"- "extract-gps-coordinates-and-plus-code"- "extract-category-tags-and-listing-photo"- "localise-results-by-language"- "deliver-places-to-mcp-connector-or-crm"unsupported_tasks:- "read-individual-review-text-or-reviewer-names"- "extract-email-addresses (Google Maps does not publish them)"- "filter-by-rating-review-count-or-open-now"- "search-by-radius-or-coordinates (query text only)"- "return-more-than-what-the-live-feed-serves (~120 per query)"- "deduplicate-places-across-queries"- "return-popular-times-or-live-busyness"- "return-menus-services-or-product-listings"- "use-a-proxy (NO proxy input exists)"search_keywords:- "google maps scraper"- "google maps api alternative"- "business listings extractor"- "local business leads"- "google places scraper"- "phone number scraper"- "gps coordinates extractor"- "local seo data"- "b2b lead generation maps"- "business directory builder"synonyms:- "gmaps scraper"- "google business listing extractor"- "local business data scraper"related_concepts:["local lead generation","local SEO","market research","competitor mapping","CRM enrichment","territory planning",]input_entities:- "searchQueries (array; REQUIRED)"- "maxResults (per query, 1-500)"- "language (en | hi | fr | de | es)"- "batchSize (concurrent place pages, 1-20)"output_entities:["name","category","rating","reviewCount","address","phone","website","hours","tags","latitude","longitude","imageUrl","plusCode","url","searchQuery",]input_rules:required_fields: ["searchQueries"]query_format: >Free text exactly as you would type it into Google Maps. Location must bepart of the query string - there is no separate location, radius orcoordinate input.max_results_semantics:per_query: truerange: [1, 500]default: 20practical_ceiling: "Google Maps stops feeding results at roughly 120 per search"free_plan_cap: 5total_results_formula: "up to maxResults x number of queries"output_conventions:every_field_nullable: >Every extracted field is null when its selector does not match. OnlysearchQuery is always present.rating_is_a_string: 'e.g. "4.4" - not a number'review_count_is_a_string: 'raw label text, e.g. "(6,912)" or "6,912 reviews" - parse before arithmetic'hours_is_one_string: 'the whole weekly table joined with " | "'coordinates_are_numbers: "latitude and longitude are floats parsed from the URL"no_dedup_across_queries: "a business matching two queries yields two records - dedupe on url"limits:results_per_query: "~120 (feed-limited), schema maximum 500"free_plan_cap: 5queries_per_run: "unlimited (each capped independently)"concurrency: "batchSize, default 5, max 20"known_limitations:no_proxy_support: >There is no proxy input. Google Maps rate-limits by IP, so large or repeatedruns can start returning thin results.no_reviews_text: "review COUNT only, never individual reviews"no_emails: "Google Maps does not publish business email addresses"pricing:model: "pay-per-result"note: "exact rate is configured on the Store listing - see the Actor's Pricing tab"when_to_use: >You want local business data for a keyword and place - to build a lead list,map competitors in a territory, enrich a CRM with phone and website, orresearch a local market - without setting up Google Cloud billing.when_not_to_use: >You need review text, email addresses, radius/coordinate search, filtering byrating or open-now, or more places than the live feed will serve.alternatives:- "Use techforce.global/google-maps-leads-sales-intelligence-tool for the same data PLUS website grading and richer sales-intelligence scoring"- "Use techforce.global/construction-lead-scraper, finance-lead-scraper, healthcare-lead-scraper or hospitality-lead-scraper for vertical-tuned Google Maps lead lists"- "Use techforce.global/website-contact-scraper-emails-phone-numbers-social-links on the `website` field to get the emails Google Maps never provides"- "Use the Google Places API directly when its licensing terms are what you need"
What this Actor does
- Takes plain Google Maps search queries —
"restaurants in Mumbai","dentists near Ahmedabad"— and runs each one separately, so a single run can cover several keywords or several cities. - Scrolls the live result feed rather than paging an API, collecting place URLs until your
maxResultsis reached or the feed stops producing new entries (it detects the end-of-list marker and a no-change streak). - Opens each place page and reads 14 fields from the DOM, including the ones Google's own API bills as premium extras: phone number, website, opening hours and the listing photo.
- Extracts GPS coordinates from the URL as real floats, plus the Plus Code — enough to map, geofence or distance-sort the results directly.
- Uses multiple selector fallbacks for the fragile fields.
ratingandreviewCounteach try three or four different selectors before giving up, because Google rotates its markup. - Stamps every row with the query that produced it (
searchQuery), so a multi-query run stays attributable. - Optionally pushes places to an authorized MCP connector (Airtable, Notion, Slack, Sheets, HubSpot, or any MCP-compatible connector) per place, chunked, or as one digest — and logs the connector tool's expected argument schema, so a misconfigured
mcpArgumentsis diagnosable from the run log. The dataset is always saved regardless of delivery settings.
ℹ️ Sibling with more analysis: if you want the same listings plus website grading and sales-intelligence scoring, use Google Maps Business Leads & Sales Intelligence. This Actor is the lean, fast extractor — one row per place, no scoring.
When to use it — and when not to
| Scenario | Fit |
|---|---|
| Building a local lead list with phones and websites | ✅ Strong (the core use case) |
| Mapping competitors in a city or territory | ✅ Strong — coordinates included |
| Enriching a CRM with business contact details | ✅ Strong (deliveryMode: "perPlace") |
| Local SEO research — categories, ratings, review volume | ✅ Strong |
| Multi-city or multi-keyword sweeps in one run | ✅ Strong — searchQueries is an array |
| Plotting results on a map or geofencing | ✅ Strong — latitude/longitude as floats |
| Non-English markets | ✅ Strong — 5 interface languages |
| Avoiding Google Cloud billing setup | ✅ Strong — no API key at all |
| Email addresses | ❌ Google Maps doesn't publish them — ➡️ Website Contact Scraper on the website field |
| Individual review text or reviewer names | ❌ Review count only |
| Filtering by rating, review count or "open now" | ❌ No such input — filter the dataset afterwards |
| Radius or coordinate-based search | ❌ Query text only — put the location in the query |
| More than ~120 results for one query | ❌ Google's feed stops there, whatever maxResults says |
| Deduplicating a business found by two queries | ❌ Not done — dedupe on url |
| Popular times / live busyness | ❌ Not extracted |
| Menus, services or product listings | ❌ Not extracted |
| Very large or repeated scheduled runs | ⚠️ No proxy support — Google rate-limits by IP |
⚡ Execution Rules (Avoid Common Agent Failures)
searchQueriesis required and is an array. The location goes inside the query string —"dentists in Ahmedabad", not a separate location field. There is no radius, coordinate or "near me" input.maxResultsis per query, not per run. Three queries at 50 is up to 150 records — and 150 billable results.- Google's feed realistically stops at ~120 results per search, regardless of
maxResults(schema max 500). Asking for 500 does not produce 500. To get more coverage, split into narrower queries (by neighbourhood, by sub-category), not a bigger number. - Free Apify plans are capped at 5 results per query. That's a successful run.
- Every extracted field can be
null. Google Maps listings are wildly inconsistent — many have no phone, no website, no hours, no rating. OnlysearchQueryis guaranteed present. Null-guard everything. ratingis a string, not a number ("4.4"). Parse before comparing.reviewCountis raw label text, and its shape varies by selector and language —"(6,912)","6,912 reviews","6.912 Rezensionen". Strip non-digits before arithmetic; don't assume a format.hoursis one long string, the whole weekly table joined with" | "— not a structured object or per-day fields.latitude/longitudeare real floats, parsed from the@lat,lngsegment of the URL. They'renullif that segment is missing.- No deduplication across queries. A business matching
"cafes in Mumbai"and"coffee shops in Mumbai"appears twice, with the sameurl. Dedupe onurlif you're merging. - No emails. Google Maps doesn't publish them. Feed
websiteinto Website Contact Scraper if you need addresses. languagechanges the data, not just the UI — categories, hour labels and review wording come back localised. Pick it deliberately if you're merging runs.batchSizeis the concurrency dial. Default 5, max 20. Higher is faster but uses more memory and is more likely to trip Google's rate limiting.- A failed place is skipped, not retried. Each place gets one attempt with a 30s navigation timeout and an 8s selector timeout; failures log a warning and the run continues. Fewer results than expected is often this.
- There is no proxy input. Google Maps rate-limits by IP, so large or repeated runs can start returning thin results with no configuration to route around it.
deliveryModedefaults toperPlace, notnone. With a connector selected, a 150-place run makes 150 connector calls. Usechunkedorsummary.
dependencies:- rule: "searchQueries is required; location must be inside the query text"on_violation: "no location/radius/coordinate input exists - a query without a place returns whatever Google guesses"- rule: "maxResults is per query and multiplies by the query count"on_violation: "volume and cost under-estimated by a factor of the query count"- rule: "Google's feed serves roughly 120 results per search regardless of maxResults"on_violation: "expecting 500 results from one query"- rule: "every extracted field can be null"on_violation: "TypeError on .strip()/.lower() of a missing phone, website or rating"- rule: "rating and reviewCount are strings, not numbers"on_violation: "numeric comparison silently fails or throws"- rule: "reviewCount format varies by selector and language"on_violation: "a regex tuned to one format drops counts from another"- rule: "no deduplication across queries"on_violation: "the same business counted - and billed - twice"- rule: "no email addresses are available from Google Maps"on_violation: "an email-enrichment step expects a field that never exists"- rule: "a failed place page is skipped without retry"on_violation: "a short result set read as a scrape failure"- rule: "no proxy input exists; Google rate-limits by IP"on_violation: "thin results on repeated runs, with no configuration to fix it"- rule: "delivery requires mcpConnector AND mcpTool"on_violation: "delivery step is skipped with a warning; the dataset is still written in full"- rule: "mcpArguments must reference {message} for the rendered template to appear anywhere"on_violation: "message body is empty; the tool is still called"- rule: "mcpConnector only resolves when the Actor runs on the Apify platform"on_violation: "local runs log 'APIFY_MCP_PROXY_URL is not set' and skip delivery; the dataset is unaffected"authentication:actor_input: noneplatform: "Apify API token required for API / SDK / MCP invocation"target_site: "no Google account, API key, billing project or cookie required - and none is accepted"
📥 Input Contract
.actor/input_schema.json is authoritative — if any table on this page disagrees with the schema files, the schema files win.
Parameters
| Field | Type | Required | Default | Allowed Values / Format | Example |
|---|---|---|---|---|---|
searchQueries | array | ✅ Yes | ["restaurants in Mumbai"] | Free-text Google Maps searches, location included | ["dentists in Ahmedabad"] |
maxResults | integer | ⬜ No | 20 | 1–500 per query; feed-limited to ~120; free plans capped at 5 | 100 |
language | enum | ⬜ No | "en" | en, hi, fr, de, es | "de" |
batchSize | integer | ⬜ No | 5 | 1–20 concurrent place pages | 8 |
Writing good queries
The location lives inside the query — there is no separate location field.
| Goal | Query |
|---|---|
| A category in a city | "restaurants in Mumbai" |
| A category near a place | "dentists near Ahmedabad" |
| A neighbourhood (better coverage than a whole city) | "cafes in Bandra West Mumbai" |
| A named chain | "Starbucks in Berlin" |
| A trade in a region | "plumbers in North London" |
💡 To get past the ~120-result ceiling, split the query, don't raise
maxResults. Six neighbourhood queries will return far more of a city's businesses than one city-wide query withmaxResults: 500.
Language
language is applied three ways — the Accept-Language header, Chromium's --lang flag, and the hl= URL parameter — so it genuinely changes the returned data, not just the chrome around it.
| Value | Language | Affects |
|---|---|---|
en | English (default) | category, hours labels, reviewCount wording |
hi | Hindi | same |
fr | French | same |
de | German | same |
es | Spanish | same |
Examples
Standard lead-list run
{"searchQueries": ["dentists in Ahmedabad"],"maxResults": 100,"language": "en","batchSize": 5,"deliveryMode": "none"}
Multi-city sweep — one run, three queries
{"searchQueries": ["coworking spaces in Mumbai","coworking spaces in Pune","coworking spaces in Bengaluru"],"maxResults": 100,"deliveryMode": "none"}
Neighbourhood split — beats one city-wide query
{"searchQueries": ["cafes in Bandra West Mumbai","cafes in Andheri West Mumbai","cafes in Lower Parel Mumbai","cafes in Colaba Mumbai"],"maxResults": 100,"deliveryMode": "none"}
Non-English market
{"searchQueries": ["Zahnärzte in Berlin"],"language": "de","maxResults": 100,"deliveryMode": "none"}
Cheap validation run before committing
{"searchQueries": ["plumbers in Leeds"],"maxResults": 5,"deliveryMode": "none"}
MCP Delivery Configuration (Optional)
Delivery pushes business data into your own tools. It never contacts a business.
| Parameter | Type | Default | Description |
|---|---|---|---|
mcpConnector | string | "" | Authorized MCP connector on your Apify account (Airtable, Notion, Slack, Sheets, HubSpot, …). Leave empty for dataset-only runs. |
deliveryMode | enum | perPlace | perPlace (one call per business) · chunked (split across calls) · summary (one call with everything) · none. |
mcpTool | string | "" | Tool name on the connector: create_record, create_page, send_message, append_row. Run once with a connector selected — the log lists the available tools and the chosen tool's expected argument schema. |
mcpArguments | object | {} | Arguments passed to the tool. String leaves support {placeholders}. |
mcpMessageTemplate | string | "" | Template rendered and exposed to mcpArguments as {message}. |
Per-place placeholders: every output field — {name}, {category}, {rating}, {reviewCount}, {address}, {phone}, {website}, {hours}, {tags}, {latitude}, {longitude}, {imageUrl}, {plusCode}, {url}, {searchQuery} — plus {coordinates} (a ready-made lat,lng string), {place} (a formatted block) and {message}.
Summary / chunked placeholders: {placeCount}, {queries}, {language}, {withPhone}, {withWebsite}, {places}, {part}, {partCount}, {message}.
One Airtable row per business — a CRM-ready lead list:
{"searchQueries": ["dentists in Ahmedabad"],"maxResults": 100,"mcpConnector": "<your-authorized-airtable-connector>","deliveryMode": "perPlace","mcpTool": "create_record","mcpArguments": {"fields": {"Business": "{name}","Category": "{category}","Phone": "{phone}","Website": "{website}","Address": "{address}","Rating": "{rating}","Reviews": "{reviewCount}","Coordinates": "{coordinates}","Maps": "{url}","From query": "{searchQuery}"}}}
A Slack digest per run:
{"searchQueries": ["coworking spaces in Pune"],"maxResults": 100,"mcpConnector": "<your-authorized-slack-connector>","deliveryMode": "summary","mcpTool": "send_message","mcpArguments": { "channel": "#leads", "text": "{message}" },"mcpMessageTemplate": "📍 {placeCount} places for {queries} — {withPhone} with a phone, {withWebsite} with a website:\n\n{places}"}
A large sweep split across Notion sub-pages:
{"searchQueries": ["restaurants in Mumbai", "restaurants in Delhi"],"maxResults": 100,"mcpConnector": "<your-authorized-notion-connector>","deliveryMode": "chunked","mcpTool": "notion-create-pages","mcpArguments": {"parent": { "page_id": "<your-page-id>" },"pages": [{"properties": {"title": "Google Maps — {queries} — part {part}/{partCount}"},"content": "{places}"}]}}
One Google Sheets row per business:
{"searchQueries": ["plumbers in Leeds"],"maxResults": 100,"mcpConnector": "<your-authorized-sheets-connector>","deliveryMode": "perPlace","mcpTool": "append_row","mcpArguments": {"values": ["{name}","{category}","{phone}","{website}","{address}","{rating}","{url}"]}}
📤 Output Contract & Data Structure
Storage: Apify Dataset — one JSON object per place, pushed as each place page finishes.
Pagination: limit & offset on the dataset items endpoint.
Duplicates: not deduplicated across queries. The same business matched by two queries produces two records sharing a url.
Place record — 15 fields
| Field | Type | Nullable | Meaning |
|---|---|---|---|
name | string | ✅ | Business name |
category | string | ✅ | Primary Google Maps category — localised by language |
rating | string | ✅ | Average rating, e.g. "4.4" — not a number |
reviewCount | string | ✅ | Raw label text: "(6,912)", "6,912 reviews", … — format varies |
address | string | ✅ | Full street address |
phone | string | ✅ | Phone number as displayed, in local format |
website | string | ✅ | Business website — the field to feed contact enrichment |
hours | string | ✅ | Whole weekly table joined with " | " — one string, not structured |
tags | array of string | ✅ | Popular attributes from the listing (e.g. ["Dinner","Reservations"]); null when none |
latitude | number | ✅ | Float parsed from the URL's @lat,lng |
longitude | number | ✅ | Float parsed from the URL's @lat,lng |
imageUrl | string | ✅ | Listing photo URL |
plusCode | string | ✅ | Google Plus Code |
url | string | ✅ | Google Maps place URL — use as the dedupe key |
searchQuery | string | never null | The query that produced this row — for attribution in multi-query runs |
⚠️ Everything except
searchQuerycan benull. Google Maps listings vary enormously: plenty of businesses have no website, no phone, no published hours, and brand-new listings have no rating. Anullis normal data, not a scrape failure.
No email addresses — Google Maps doesn't publish them. No review text — only the count. No popular times, menus, services or price level.
Example — a well-populated listing
{"name": "Trishna","category": "Seafood restaurant","rating": "4.4","reviewCount": "(6,912)","address": "7 Rope Walk Ln, Kala Ghoda, Fort, Mumbai, Maharashtra 400001","phone": "022 2270 3213","website": "https://trishna.co.in/","hours": "Monday | 12:00–3:30 pm, 6:30–11:45 pm | Tuesday | 12:00–3:30 pm, 6:30–11:45 pm","tags": ["Dinner", "Reservations", "Takeaway"],"latitude": 18.9285,"longitude": 72.8321,"imageUrl": "https://lh5.googleusercontent.com/p/AF1QipN…","plusCode": "WQ8V+3M Mumbai, Maharashtra","url": "https://www.google.com/maps/place/Trishna/@18.9285,72.8321,17z/","searchQuery": "restaurants in Mumbai"}
Example — a sparse listing (equally valid)
{"name": "Sharma Dental Clinic","category": "Dentist","rating": null,"reviewCount": null,"address": "Shop 4, Naranpura, Ahmedabad, Gujarat 380013","phone": "098250 12345","website": null,"hours": null,"tags": null,"latitude": 23.0561,"longitude": 72.5619,"imageUrl": null,"plusCode": null,"url": "https://www.google.com/maps/place/Sharma+Dental+Clinic/@23.0561,72.5619,17z/","searchQuery": "dentists in Ahmedabad"}
Making the output usable
import redef rating_value(place):raw = place.get("rating") # string or Nonetry:return float(raw) if raw else Noneexcept ValueError:return None # localised decimal comma, etc.def review_total(place):raw = place.get("reviewCount") or "" # "(6,912)" / "6,912 reviews" / ...digits = re.sub(r"[^\d]", "", raw)return int(digits) if digits else None# Null-guard everything except searchQuery.contactable = [p for p in items if p.get("phone") or p.get("website")]# No dedup across queries — the same business can appear once per matching query.unique = {p["url"]: p for p in items if p.get("url")}print(f"{len(items)} rows → {len(unique)} unique businesses")
What a successful run looks like
{"status": "SUCCEEDED","defaultDatasetId": "<datasetId>","stats": { "itemCount": 87 }}
itemCount below maxResults × len(searchQueries) is normal — the feed ran out, or individual place pages were skipped. itemCount: 0 is a valid empty result. The log distinguishes the causes:
| Log line | Meaning |
|---|---|
🔍 Query: <q> | Language: <lang> | That query started |
↳ URLs found: N | Per-scroll progress collecting place URLs |
↳ No more results. | The feed stopped producing new entries — a complete sweep |
↳ Reached end of list. | Google's end-of-list marker appeared — a complete sweep |
Feed not found for '<q>': … | The result feed never rendered — bad query, or Google served a block/consent page |
No URLs collected for '<q>' | The query matched nothing |
🚀 Scraping N places in batches of M… | Detail phase started |
[i] ✅ <name> | That place was extracted and saved |
[i] ⚠️ No data extracted | The page loaded but the extractor returned nothing |
[i] ❌ <url>… → <error> | That place page failed (timeout or navigation error) and was skipped without retry |
📦 N place(s) saved across M query/queries. | Final count |
Free plan detected — capping results to 5 per query. | Free-plan cap applied |
▶️ Invocation & Integration
Lifecycle: Discover → Understand → Configure → Execute → Retrieve → Validate. Authenticate every call with Authorization: Bearer <APIFY_TOKEN>.
Apify Console
Open the Actor → Input tab → add one or more Search Queries (location included in the text) → set Max Results Per Query → pick a Language → (optional) adjust Batch size → (optional) pick an MCP connector, tool and template → Start → export from the Dataset tab (JSON, CSV, Excel, XML). The Contacts only view is CRM-ready; Map & media shows coordinates and photos.
Model Context Protocol (MCP)
claude mcp add --transport http apify \"https://mcp.apify.com?tools=techforce.global/google-maps-scraper"
Python (apify-client)
import os, refrom apify_client import ApifyClientclient = ApifyClient(os.getenv("APIFY_TOKEN"))ACTOR = "techforce.global/google-maps-scraper"run = client.actor(ACTOR).call(run_input={"searchQueries": ["dentists in Ahmedabad", "orthodontists in Ahmedabad"],"maxResults": 100, # PER QUERY → up to 200 rows here"language": "en","batchSize": 5,"deliveryMode": "none",})items = client.dataset(run["defaultDatasetId"]).list_items().itemsprint(f"{len(items)} rows")def review_total(place):digits = re.sub(r"[^\d]", "", place.get("reviewCount") or "")return int(digits) if digits else 0# No cross-query dedup is done for you.unique = list({p["url"]: p for p in items if p.get("url")}.values())contactable = [p for p in unique if p.get("phone") or p.get("website")]print(f"{len(unique)} unique · {len(contactable)} with a phone or website")for place in sorted(contactable, key=review_total, reverse=True)[:10]:print(f'{place["name"]} · {place.get("rating") or "—"}★ ({review_total(place)} reviews)')print(f' {place.get("phone") or "no phone"} · {place.get("website") or "no website"}')
Beating the ~120-per-query ceiling — split by neighbourhood rather than raising maxResults:
AREAS = ["Bandra West", "Andheri West", "Lower Parel", "Colaba", "Powai"]run = client.actor(ACTOR).call(run_input={"searchQueries": [f"cafes in {area} Mumbai" for area in AREAS],"maxResults": 100,"deliveryMode": "none",})rows = client.dataset(run["defaultDatasetId"]).list_items().itemsunique = {p["url"]: p for p in rows if p.get("url")}print(f"{len(rows)} rows → {len(unique)} unique cafes across {len(AREAS)} areas")
JavaScript / TypeScript (apify-client)
import { ApifyClient } from "apify-client";const client = new ApifyClient({ token: process.env.APIFY_TOKEN });const ACTOR = "techforce.global/google-maps-scraper";const run = await client.actor(ACTOR).call({searchQueries: ["plumbers in Leeds"],maxResults: 100,language: "en",deliveryMode: "none",});const { items } = await client.dataset(run.defaultDatasetId).listItems();// Everything except searchQuery is nullable; rating/reviewCount are STRINGS.const withSite = items.filter((p) => p.website);const rated = items.filter((p) => p.rating).map((p) => ({ name: p.name, rating: parseFloat(p.rating) })).filter((p) => !Number.isNaN(p.rating)).sort((a, b) => b.rating - a.rating);console.log(`${items.length} places · ${withSite.length} with a website`);console.table(rated.slice(0, 10));
cURL — asynchronous with a spend cap (recommended)
Browser-based runs over many places take minutes, so prefer async over the 300s synchronous endpoint.
# 1. Start execution with a hard spend limitcurl -X POST -H 'Content-Type: application/json' \-H 'Authorization: Bearer <YOUR_APIFY_TOKEN>' \-d '{"searchQueries":["dentists in Ahmedabad"],"maxResults":100,"language":"en","deliveryMode":"none"}' \'https://api.apify.com/v2/acts/techforce.global~google-maps-scraper/runs?maxTotalChargeUsd=2.00'# 2. Pollcurl -H 'Authorization: Bearer <YOUR_APIFY_TOKEN>' \'https://api.apify.com/v2/actor-runs/<runId>'# 3. Retrieve when SUCCEEDEDcurl -H 'Authorization: Bearer <YOUR_APIFY_TOKEN>' \'https://api.apify.com/v2/datasets/<DATASET_ID>/items?clean=true&format=json'
cURL — synchronous (small runs only)
curl -X POST \-H 'Content-Type: application/json' \-H 'Authorization: Bearer <YOUR_APIFY_TOKEN>' \-d '{"searchQueries":["plumbers in Leeds"],"maxResults":10,"deliveryMode":"none"}' \'https://api.apify.com/v2/acts/techforce.global~google-maps-scraper/run-sync-get-dataset-items'
⚠️ Autonomous Agent Error Handling & Resolution Matrix
| Error Code | Detection Condition | Underlying Cause | Deterministic Agent Action |
|---|---|---|---|
AUTH_INVALID | HTTP 401 from Apify | Missing or malformed APIFY_TOKEN | Abort & Prompt User: request a valid Apify API token. |
RATE_LIMITED | HTTP 429 from Apify | Apify API rate / concurrency limits | Retry: exponential backoff (2s, 4s, 8s). |
SYNC_TIMEOUT | HTTP 408 on the sync endpoint | Browser run over many places exceeds 300s | Reconfigure: async POST /runs + polling. |
MISSING_QUERIES | Input validation rejection | searchQueries is required | Modify Input: supply an array with at least one query. |
NO_LOCATION_IN_QUERY | Results from unexpected places | The location must be inside the query string | Modify Input: "dentists in Ahmedabad", not "dentists". |
FEED_NOT_FOUND | Log Feed not found for '<q>' | The result feed never rendered — malformed query, or Google served a consent/block page | Retry once; simplify the query; if it persists, the IP is likely blocked. |
NO_URLS_COLLECTED | Log No URLs collected for '<q>' | The query genuinely matched nothing | Modify Input: broaden or correct the query. |
VOLUME_UNDERESTIMATED | Far more rows than expected | maxResults is per query | Fix Expectation: total ≈ maxResults × query count. |
FEED_CEILING | Asked for 500, got ~120 | Google's feed stops serving there | Reconfigure: split into narrower queries by neighbourhood or sub-category. |
FREE_PLAN_CAP | 5 rows per query, upgrade banner in the log | Free Apify plan cap | Prompt User: upgrade for full-volume runs. |
PLACE_SKIPPED | Log [i] ❌ <url>… → <error> | That place page timed out or failed; no retry | Not a failure. Re-run if you need that specific place. |
EXTRACTOR_EMPTY | Log [i] ⚠️ No data extracted | The page loaded but no selector matched — Google markup change | Retry; if widespread, report it — selectors need updating. |
NULL_FIELDS | phone/website/rating are null | Those listings genuinely don't publish them | Not an error. Null-guard and filter. |
RATING_NOT_NUMERIC | Comparison on rating fails | It's a string ("4.4") | Fix Code: float() it, and handle localised decimals. |
REVIEWCOUNT_FORMAT | Parsing reviewCount fails | Raw label text, format varies by selector and language | Fix Code: strip non-digits before int(). |
HOURS_NOT_STRUCTURED | Expecting per-day fields | hours is one joined string | Fix Expectation: parse the string, or open url. |
DUPLICATE_ACROSS_QUERIES | The same business twice | No cross-query dedup | Fix Code: dedupe on url. |
NO_EMAIL_FIELD | No email anywhere | Google Maps doesn't publish emails | Re-route: Website Contact Scraper on website. |
NO_REVIEW_TEXT | User wants review content | Only the count is extracted | Fix Expectation: not supported. |
FILTER_UNSUPPORTED | Agent passes minRating, openNow, radius, … | No such inputs exist | Fix Approach: run unfiltered, filter the dataset. |
THROTTLED_BY_GOOGLE | Sharply fewer results than an earlier identical run | Google rate-limits by IP; no proxy input exists | Wait & Retry later; lower batchSize and maxResults. |
DELIVERY_SKIPPED | Log no tool name was provided | mcpConnector set but mcpTool empty | Modify Input: set mcpTool; the dataset is already saved. |
DELIVERY_TOOL_UNKNOWN | Log Tool 'x' is not available on this connector | Wrong tool name for that connector | Modify Input: pick a name from the Available tools: list in the same log line. |
DELIVERY_ARGS_INVALID | Connector rejects the arguments | mcpArguments doesn't match the tool's schema | Read the log: the line Connector [<tool>] expects arguments matching this schema: prints the exact shape. |
DELIVERY_EMPTY | Connector called, body empty | mcpArguments omitted the {message} placeholder | Modify Input: map {message} inside mcpArguments. |
DELIVERY_FLOOD | Hundreds of connector calls | deliveryMode defaults to perPlace | Reconfigure: use chunked or summary. |
MCP_PROXY_MISSING | Log APIFY_MCP_PROXY_URL is not set | Running locally instead of on the platform | Reconfigure: apify push and run on the platform. |
CHARGE_LIMIT | Run aborted mid-sweep | maxTotalChargeUsd spend threshold reached | Prompt User: confirm a higher spend cap. |
⚠️ Verify before publishing: the Apify HTTP status rows (
401,408,429) reflect standard platform behavior. Every Google-side row, quoted log line and field condition is taken from this Actor's source. Re-confirm the platform rows if you depend on them for automated retry logic.
🗣️ Natural Language → Actor Mapping
| User says | Intent | Constructed Actor input |
|---|---|---|
| "Find me all the dentists in Ahmedabad" | Lead list | {"searchQueries":["dentists in Ahmedabad"],"maxResults":100} |
| "Get restaurants in Mumbai with phone numbers" | Lead list | run, then filter phone |
| "Build a list of plumbers across three cities" | Multi-city | {"searchQueries":["plumbers in Leeds","plumbers in York","plumbers in Hull"]} |
| "Who are my competitors in this area?" | Competitor mapping | {"searchQueries":["<category> in <area>"],"maxResults":100} |
| "Map every cafe in Mumbai" | Wide coverage | Split by neighbourhood — one query per area, then dedupe on url |
| "Get GPS coordinates for these businesses" | Geo data | run — latitude/longitude are floats in every record |
| "Which of these have no website?" | Gap analysis | run, then filter website == null — a classic agency pitch list |
| "Highest-rated gyms in Berlin" | Ranking | {"searchQueries":["gyms in Berlin"]} then sort parsed rating |
| "Get opening hours for local pharmacies" | Hours | run — hours is one joined string per place |
| "Search in German" | Localisation | {"language":"de"} |
| "Push these leads into Airtable" | CRM delivery | {"mcpConnector":"airtable","deliveryMode":"perPlace","mcpTool":"create_record"} |
| "Post a summary to Slack" | Digest delivery | {"mcpConnector":"slack","deliveryMode":"summary","mcpTool":"send_message"} |
| "Get their email addresses" | Contact enrichment | ⚠️ Not on Google Maps — ➡️ Website Contact Scraper on website |
| "What do the reviews say?" | Review text | ❌ Review count only |
| "Only places rated above 4 stars" | Filtering | ⚠️ No filter input — filter the dataset on parsed rating |
| "Everything within 5 km of this point" | Radius search | ❌ No radius or coordinate input — put the area in the query |
| "Give me 500 results for one search" | Volume | ⚠️ Google's feed stops near 120 — split the query instead |
| "Show me how busy they are" | Popular times | ❌ Not extracted |
| "What's on their menu?" | Menu data | ❌ Not extracted |
Should NOT route here: email addresses · individual review text or reviewer names · rating / open-now / price filtering at the source · radius or coordinate search · popular times and live busyness · menus, services or product listings · cross-query deduplication · more places than Google's live feed will serve.
🧭 Agent Execution & Routing Logic
[Input User Query]│▼1. Is the user after LOCAL BUSINESS data (a category + a place)? NO → Route elsewhere│ YES▼2. What do they actually need?├── name/address/phone/website/rating/hours/coords → stay here├── EMAIL addresses → run here, then Website Contact Scraper on `website`├── review TEXT → Abort: only the review COUNT is available├── website grading / scoring→ Route: Google Maps Leads & Sales Intelligence└── popular times, menus → Abort: not extracted▼3. Build the queries — THE LOCATION GOES IN THE QUERY TEXT.There is no location, radius or coordinate field."dentists in Ahmedabad" ✅ "dentists" + location param ❌▼4. How much coverage is needed?├── a sample → one query, maxResults 20-50└── a whole city → SPLIT BY NEIGHBOURHOOD, one query each.Google's feed stops near 120 per query, so raisingmaxResults to 500 does NOT get you more.▼5. Set maxResults PER QUERY (free plans 5). Total ≈ maxResults × query count = billable rows▼6. Non-English market? set language (en|hi|fr|de|es) — it changes category and hour text▼7. Deliver to a connector? YES → mcpConnector + mcpTool + {message}│ perPlace (1 call each!) / chunked / summary│ NO → set deliveryMode "none" (it defaults to perPlace)▼8. Use the ASYNC endpoint — browser runs over many places exceed 300s▼[Execute]│├──► SUCCEEDED, items > 0 ──► handle the field contract:│ EVERYTHING except searchQuery is nullable → null-guard first│ rating and reviewCount are STRINGS → parse before comparing│ reviewCount format varies by language → strip non-digits│ hours is ONE joined string → not per-day fields│ NO cross-query dedup → dedupe on `url`│ log has "[i] ❌"? → those places were skipped, no retry│└──► SUCCEEDED, items == 0 ──► READ THE PER-QUERY LOG LINES:"Feed not found for '<q>'" → feed never rendered: bad query, or IP blocked"No URLs collected for '<q>'"→ the query matched nothing: broaden it
💰 Cost Control & Pricing Transparency
Pay-per-result: you are charged per place returned, with no monthly subscription. The exact rate is on the Actor's Pricing tab in the Apify Store.
Free Apify plans are capped at 5 results per query, so you can validate a query set for a handful of results before a full sweep.
Autonomous spend caps: pass maxTotalChargeUsd as a query parameter on the run endpoint for a hard per-execution ceiling.
⚠️ Verify before publishing: this repo contains no
.actor/pay_per_event.jsonand the code makes noActor.charge()calls, so billing is configured entirely on the Store listing. Confirm the live pricing model and rate before publishing.
The multiplier to watch: maxResults is per query
| Queries | maxResults | Maximum rows (= billable results) |
|---|---|---|
| 1 | 20 (default) | 20 |
| 1 | 100 | ~100 (feed permitting) |
| 3 | 100 | ~300 |
| 6 (neighbourhood split) | 100 | ~600 |
| Any | 5 (free plan) | 5 × query count |
And duplicates are billed. Overlapping queries ("cafes in Mumbai" + "coffee shops in Mumbai") will return the same businesses twice, and you pay for both rows — dedupe on url before counting your real coverage.
Platform usage is billed on top, and it dominates here: this is a real Chromium browser opening one page per place. A 100-place query is 100 page loads plus the feed scroll.
Cost levers:
- Validate at
maxResults: 5before a full sweep — a bad query costs a whole run otherwise. - Don't raise
maxResultsabove ~120. You pay for the attempt, not the ceiling, but you also won't get more — split the query instead. - Avoid overlapping queries. Synonym queries duplicate rows and double the bill for the same businesses.
- Tune
batchSize. Higher finishes sooner (less compute time) but risks Google throttling; 5–8 is the practical band. - Prefer
chunked/summarydelivery overperPlace— 1 connector call instead of hundreds. - Cap the spend with
maxTotalChargeUsdon any agent-driven run.
🔍 Companion machine-readable files
| File | Purpose |
|---|---|
.actor/actor.json | Identity, title, description, version, build tag, and the input / output / dataset schema references |
.actor/input_schema.json | Authoritative typed input contract — searchQueries (required), maxResults (1–500 per query), the 5-value language enum, batchSize (1–20), and the 5 delivery fields |
.actor/dataset_schema.json | Three Console Output views — Overview, a CRM-ready Contacts only, and Map & media for coordinates and photos |
.actor/output_schema.json | Declares where results are stored |
my_actor/main.py | Query building and localisation, feed scrolling, place-URL collection, batched detail scraping, the in-page extractor, MCP delivery |
my_actor/delivery.py | MCP delivery: placeholder flattening, place-block rendering, chunk grouping, per-mode dispatch |
my_actor/connector.py | MCP connector session handling through the Apify MCP Proxy, including the tool input schemas it logs |
Note: this Actor has no pay_per_event.json and makes no Actor.charge() calls — billing is configured entirely on the Store listing.
If any table on this page disagrees with the schema files, the schema files win.
🛠️ Troubleshooting
| # | Symptom you see | Most likely cause | Fix |
|---|---|---|---|
| 1 | 0 rows; log Feed not found for '<q>' | The result feed never rendered — malformed query, or Google served a consent/block page | Simplify the query and retry; if it persists, the container IP is likely blocked. |
| 2 | 0 rows; log No URLs collected for '<q>' | The query genuinely matched nothing | Broaden or correct the query. |
| 3 | Results from the wrong place | The location wasn't in the query text | Use "dentists in Ahmedabad" — there's no separate location field. |
| 4 | Far more rows than expected | maxResults is per query | Total ≈ maxResults × query count. |
| 5 | Asked for 500, got ~120 | Google's feed stops serving around there | Split into narrower queries; don't raise the number. |
| 6 | Exactly 5 per query | Free Apify plan cap | Upgrade; the log prints the upgrade banner. |
| 7 | Some places missing; log shows [i] ❌ | Those pages timed out and were skipped without retry | Re-run for those; lower batchSize if it's frequent. |
| 8 | Log [i] ⚠️ No data extracted | Page loaded but no selector matched — Google changed its markup | Retry; if widespread, report it so selectors can be updated. |
| 9 | phone/website/rating are null | Those listings don't publish them | Normal. Null-guard and filter. |
| 10 | rating comparison fails | It's a string ("4.4") | float() it; watch for localised decimal commas. |
| 11 | reviewCount won't parse | Raw label text, varies by selector and language | Strip non-digits before int(). |
| 12 | hours isn't per-day | It's one string joined with " | " | Parse the string, or open url. |
| 13 | The same business appears twice | Overlapping queries; no cross-query dedup | Dedupe on url — and note you were billed for both. |
| 14 | No email addresses | Google Maps doesn't publish them | Feed website into Website Contact Scraper. |
| 15 | No review text | Only the count is extracted | Not supported. |
| 16 | Categories in the wrong language | language controls the returned data | Set language to match your market. |
| 17 | Sharply fewer results than an earlier identical run | Google is rate-limiting the IP; no proxy support | Wait and retry; lower batchSize and maxResults. |
| 18 | Run takes many minutes | One browser page load per place | Expected; use the async endpoint and tune batchSize. |
| 19 | HTTP 408 on run-sync-get-dataset-items | The synchronous endpoint has a hard 300-second ceiling | Use async POST /runs → poll → fetch. |
| 20 | Hundreds of connector calls | deliveryMode defaults to perPlace | Switch to chunked or summary. |
| 21 | Notion / Airtable timed out | One call carrying every place is too large | Use deliveryMode: "chunked". |
| 22 | Connector rejected the arguments | mcpArguments doesn't match the tool's schema | Read the Connector [<tool>] expects arguments matching this schema: log line. |
| 23 | Slack / Airtable received nothing | Delivery needs both mcpConnector and mcpTool | Set both. The dataset is still written in full. |
| 24 | Connector fired but the body is empty | mcpArguments did not reference {message} | Map {message} and populate mcpMessageTemplate. |
| 25 | Log APIFY_MCP_PROXY_URL is not set | You ran locally; connectors only resolve on the platform | Deploy with apify push and run on the platform. |
| 26 | HTTP 401 / 403 from Apify | Missing, expired or malformed APIFY_TOKEN | Regenerate in Apify Console → Settings → API & Integrations. |
| 27 | HTTP 429 from Apify | Apify account concurrency / rate limits | Retry with exponential backoff (2s → 4s → 8s). |
Known limitations
- No proxy support. Google Maps rate-limits by IP and there's no input to route around it — the main constraint on large or scheduled runs.
- Google's feed serves roughly 120 results per query, whatever
maxResultssays. - No emails, no review text, no popular times, no menus.
- No filtering at the source — no rating, open-now, price or radius inputs.
- No cross-query deduplication, so overlapping queries produce (and bill for) duplicate rows.
- Selector-based extraction means Google markup changes can blank individual fields; the fragile fields have fallbacks, but not infinite ones.
Diagnostic checklist before opening an issue
- Baseline run.
{"searchQueries":["restaurants in Mumbai"],"maxResults":5,"deliveryMode":"none"}— a dense, well-covered query. - Rows returned? If yes, the pipeline is fine and your earlier query was the problem.
- Read the per-query log lines.
Feed not foundandNo URLs collectedmean completely different things — the first suggests a block or a malformed query, the second an empty result. - Count the
❌and⚠️lines. Many❌means timeouts (lowerbatchSize); many⚠️means the extractor found nothing, which points at a Google markup change. - Check whether fields are missing or the listings are. A
nullphone on a listing that genuinely has no phone is not a bug — open theurlto confirm.
If the issue survives all five steps, open an Issues ticket on the Actor page (or email support) with the run ID, the exact input JSON, and a url from the dataset where a field you expected was empty.
❓ FAQ
Setup & scope
Do I need a Google Maps API key?
No — and none is accepted. There's no Google Cloud project, billing account or quota to manage. The Actor reads the public Google Maps interface.
How is this different from the Google Places API?
Places bills per request with fields split across pricing tiers — phone and website sit on a more expensive SKU, and photos are billed separately again. This Actor returns all of them in every record at one pay-per-result rate, and scrolls further than the API's 60-result ceiling. The trade-off is licensing: see Compliance.
Can I plug it into Claude, Cursor, or a LangChain agent?
Yes — it is a native MCP tool:
claude mcp add --transport http apify "https://mcp.apify.com?tools=techforce.global/google-maps-scraper"
How do I search a specific area?
Put it in the query: "cafes in Bandra West Mumbai". There is no separate location, radius or coordinate input.
Coverage & volume
Why did I get ~120 results when I asked for 500?
That's Google's own feed limit for a single search — it stops producing new entries around there. The fix is more queries, not a bigger number: split by neighbourhood or sub-category and dedupe on url.
Is maxResults per query or per run?
Per query. Three queries at 100 is up to 300 rows, and 300 billable results.
Why are some places missing?
Individual place pages get one attempt with a 30-second navigation timeout; failures are logged with ❌ and skipped without retry. Lowering batchSize reduces this.
Output
Can I get email addresses?
Not from Google Maps — it doesn't publish them. Take the website field and run Website Contact Scraper over it.
Can I get the actual reviews?
No — only reviewCount. Review text and reviewer names aren't extracted.
Why is rating a string?
It's taken verbatim from the page. float() it, and note that non-English locales can use a decimal comma.
Why won't reviewCount parse as a number?
It's the raw label text and its shape depends on which selector matched and which language you chose — "(6,912)", "6,912 reviews", "6.912 Rezensionen". Strip non-digits first.
Why are so many fields null?
Because Google Maps listings genuinely vary — plenty of small businesses have no website, no published hours, and no reviews yet. Everything except searchQuery is nullable by design.
Can I tell which query a row came from?
Yes — every row carries searchQuery.
Pricing
How is this billed?
Pay-per-result — charged per place returned, no monthly subscription. The current rate is on the Actor's Pricing tab. Apify platform usage is billed separately and is significant here, since each place is a browser page load.
Do duplicate rows cost me twice?
Yes. Overlapping queries return the same business more than once and each row is billable — avoid synonym queries, and dedupe on url.
How do I guarantee an agent never overspends?
Pass maxTotalChargeUsd as a query parameter on the run endpoint, and validate queries at maxResults: 5.
🔗 Related Actors
Google Maps family
| Actor | Returns | Why pick it over this one |
|---|---|---|
| This Actor | 15 fields per place — the lean, fast extractor | Straight business data with no scoring overhead |
| Google Maps Business Leads & Sales Intelligence | The same listings plus website grading and sales-intelligence scoring | ⭐ You want leads ranked and qualified, not just extracted |
| Construction Lead Scraper | Contractors and developers, vertical-tuned | Purpose-built queries and fields for construction |
| Finance Lead Scraper | Financial firms, vertical-tuned | Purpose-built for finance prospecting |
| Healthcare Lead Scraper | Practices and clinics, vertical-tuned | Purpose-built for healthcare |
| Hospitality Lead Scraper | Venues, hotels and restaurants, vertical-tuned | Purpose-built for hospitality |
⭐ If you want leads scored, not just listed, use Google Maps Business Leads & Sales Intelligence. It builds on the same source and adds website grading and richer qualification.
Come back to this Actor when you want the raw listings — fewer moving parts, faster runs, and a flat 15-field record that drops straight into a spreadsheet or CRM.
Enrichment & downstream pipeline
| Actor | Use it for |
|---|---|
| Website Contact Scraper | The emails Google Maps never provides — feed it the website field |
| LinkedIn Company Decision Makers | Named founders, C-suite and directors at the businesses you found |
| LinkedIn Company Employees | Staff, with department and seniority |
| GOV.UK Business Scraper | Verify a UK business is a real, active company with PSC / beneficial ownership |
| Advanced Website Crawling Actor | Crawl a business's own site for deeper context |
Suggested pipeline patterns
📇 Maps → emails → CRM
This Actor for the listings → Website Contact Scraper on website → deliveryMode: "perPlace" into Airtable. Phone and email for every lead.
🎯 The "no website" pitch list
Run a category in your city → filter website == null → businesses with a Maps presence but no site. A ready-made prospect list for web agencies.
🗺️ Territory mapping
Neighbourhood-split queries → dedupe on url → plot latitude/longitude to see coverage gaps and competitor density.
🏢 Local business → named buyer This Actor for the company → LinkedIn Decision Makers for the person to address → merge. Both the route in and the name.
🛡️ Verified UK outreach list This Actor → GOV.UK Business Scraper on the business name → confirm the entity is active before anyone spends outreach time.
Browse all Actors by Techforce Global at apify.com/techforce.global.
🔐 Compliance & Data Privacy
This Actor is an independent tool. It is not affiliated with, endorsed by, or sponsored by Google LLC. "Google", "Google Maps" and related marks are trademarks of Google LLC.
It reads publicly displayed business listings on Google Maps. It does not log in, does not accept credentials, and does not use the Google Places API.
Most of the output is business data, but not all of it, and the terms question is real:
- Google's Terms of Service restrict automated access. Using this Actor is a decision to work outside the Places API's licensing, and Google's terms — including limits on caching, storing and redistributing Maps content — are your responsibility to evaluate. If your use case needs contractually clean redistribution rights, the Places API is the correct tool despite the cost.
- Sole traders' details are personal data. A plumber or consultant listed under their own name with a mobile number is an identifiable individual, not a company. GDPR, UK GDPR and CCPA/CPRA apply to those rows: have a lawful basis (legitimate interest for B2B, with a balancing assessment), record it, set a retention period, and honour access, objection and erasure requests.
- A listed phone number is not consent to be called or messaged. Business phone numbers are published so customers can make contact, which is a different purpose from marketing. Before any outreach campaign, satisfy the applicable rules — GDPR Art. 6/21, ePrivacy/PECR, CAN-SPAM, CASL, and national do-not-call registries, which in many jurisdictions cover sole traders.
- Ratings and review counts describe real businesses. Republishing them as current — in a directory, comparison site or report — commits you to a refresh cadence; a stale rating shown as current can be commercially damaging to the business and legally risky for you. Attribute the source and link through to
url.
The Actor keeps its footprint modest (batched concurrency, one page load per place), and there is no proxy support to escalate beyond that — don't defeat the design by running many parallel executions against the same queries. You remain responsible for complying with Google's Terms of Service and all applicable data-protection and marketing law in the markets you operate in.
🆘 Support & Custom Pipeline Engineering
Need proxy support for large scheduled sweeps, email enrichment wired into the pipeline, cross-query deduplication, review-text extraction, or a full local-lead-generation workflow feeding your CRM?
- Email: bhavin.shah@techforceglobal.com
- Custom Enterprise Integrations: Book a 15-Minute Technical Consultation
- Maintained by: Techforce Global — Specialists in High-Performance Web Scrapers and Agentic Workflows.
Made with ❤️ by Techforce Global Specialists in High-Performance B2B Contact Data Extraction and AI Automation.
🏷️ Structured data for search & AI discovery
{"@context": "https://schema.org","@type": "SoftwareApplication","name": "Google Maps Scraper — Business Listings, Phones, Websites & GPS Coordinates","applicationCategory": "BusinessApplication","operatingSystem": "Cloud (Apify platform)","description": "Extracts business listings from Google Maps for any search query — name, category, rating, review count, address, phone, website, opening hours, popular tags, GPS coordinates, Plus Code and listing photo — with no Google Maps API key, billing account or quota. Runs several queries per execution, scrolls the live result feed, localises results into English, Hindi, French, German or Spanish, and can deliver straight into Airtable, Notion, Slack, Google Sheets or any authorized MCP connector. Email addresses and review text are not available from Google Maps.","url": "https://apify.com/techforce.global/google-maps-scraper","offers": {"@type": "Offer","priceCurrency": "USD","description": "Pay-per-result, charged per place returned. The current rate is listed on the Actor's Pricing tab."}}