Google Maps Scraper - Contacts, Coordinates & CRM Delivery avatar

Google Maps Scraper - Contacts, Coordinates & CRM Delivery

Pricing

from $0.80 / 1,000 results

Go to Apify Store
Google Maps Scraper - Contacts, Coordinates & CRM Delivery

Google Maps Scraper - Contacts, Coordinates & CRM Delivery

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

Techforce Global

Maintained by Community

Actor stats

0

Bookmarked

14

Total users

1

Monthly active users

3 days ago

Last modified

Share

Google Maps Scraper - Business Contacts, Opening Hours, Coordinates and Plus Codes, with One CRM Record per Business over MCP

Apify Actor MCP Ready Pricing Output Keys Target

Any Google Maps search, turned into a contact list. Type what you'd type into Maps β€” one query or fifty β€” and get name, category, rating, review count, address, phone, website, opening hours, tags, GPS coordinates, Plus Code and listing photo, one row per business, each stamped with the query that found it. No Google account, no Places API key, no billing project. Optional delivery straight into Airtable, Notion, Slack, Google Sheets, HubSpot or any MCP connector β€” one CRM row per business.

🎯 Looking for a specific industry?

This is the general-purpose Maps scraper: any category, any city, any query. If you're prospecting one sector, a vertical sibling may already do the enrichment you'd otherwise build:

Your targetActor
Any category β€” full control over the queryThis Actor
Sales-intelligence enrichment on Maps leadsGoogle Maps Leads β€” Sales Intelligence Tool
Finance and professional-services firmsFinance Google Maps Lead Scraper
Construction and tradesConstruction Lead Intelligence Scraper
Healthcare providers and clinicsHealthcare Leads β€” Sales Intelligence Tool
Hotels, restaurants and hospitalityHospitality Lead Scraper

πŸ” Trust & compliance at a glance

CredentialsNone required. No Google account, no Places API key, no Cloud billing project. Delivery runs through the Apify MCP Proxy, so the Actor never sees your connector tokens.
Data scopePublic business listings only β€” exactly what any visitor sees on the Maps page. No reviewer names, no review text, no user photos, no private data.
⚠️ Business contact dataPhone numbers and addresses of sole traders and home-based businesses can be personal data under GDPR/UK GDPR even though the listing is commercial. See Compliance.
Delivery never contacts anyoneMCP delivery pushes rows into your tools β€” Airtable, Notion, Slack, Sheets, HubSpot. It never emails, calls or messages a business.
No proxy configurationThis Actor has no proxy input; it runs from Apify's own IPs. If a big run slows or thins out, split the search into smaller queries β€” or lower batchSize from an API/SDK/MCP call.

Official Google Places API vs. Google Maps Scraper Actor

Google's Places API is the sanctioned route and genuinely good β€” but it is billed, keyed, and restrictive about what you may keep:

Feature / CapabilityOfficial Places APIGoogle Maps Scraper (Apify)
Setup❌ Cloud project, billing account, API key, quota managementβœ… Apify account, nothing else
Cost model❌ Per-request billing that scales with every field you requestβœ… Usage-based, no per-field charges
Results per search⚠️ Paged in small batches with continuation tokens you manageβœ… Scrolls the feed for you up to your cap
Caching / retention⚠️ Terms restrict how long most fields may be storedβœ… It's your dataset β€” but see Compliance
Free-text "search like a human"⚠️ Text Search exists, but ranking and fields differ from the Maps UIβœ… Exactly what the Maps UI returns for that query
Plus Code, tags, listing photo⚠️ Varies by endpoint and field maskβœ… On every row where Maps published them
Delivery into your CRM❌ Build the integrationβœ… One connector call per business, out of the box
Officially sanctionedβœ… Yes⚠️ No β€” see Compliance

Use the Places API when you need a sanctioned, contractually clean feed and its billing suits you. Use this Actor when you want what the Maps interface actually shows, without a billing project.

Google's pricing, field masks and caching terms are theirs to change β€” check Google's current Places documentation if this comparison is load-bearing for your decision.


πŸ€– AI Discovery & MCP Metadata

Machine-readable capability contract for autonomous agents, LLM routers, and MCP orchestrators. Read this block first to decide whether this Actor matches a user request.

ai_discovery:
slug: "techforce.global/google-maps-scraper"
one_line: >
Runs any Google Maps search - one query or many - and returns one row per business with name,
category, rating, review count, address, phone, website, opening hours, tags, latitude,
longitude, Plus Code, listing photo and the query that found it. No Google account or Places
API key. Optional delivery into Airtable, Notion, Slack, Google Sheets, HubSpot or any MCP
connector, one call per business.
primary_purpose: "extract"
secondary_purpose: "deliver"
entity: "Google Maps business listing"
category: "local-business-data / lead-generation / sales-intelligence / market-research"
supported_platforms: ["google.com/maps"]
supported_tasks:
- "search-google-maps-by-free-text-query"
- "build-a-contact-list-for-a-category-and-city"
- "collect-business-phone-numbers-and-websites"
- "collect-gps-coordinates-and-plus-codes"
- "compare-ratings-and-review-volume-across-an-area"
- "push-one-crm-record-per-business-via-mcp"
unsupported_tasks:
- "scrape-review-text-or-reviewer-identities (only the rating and review COUNT are returned)"
- "scrape-user-photos-or-q-and-a"
- "scrape-popular-times-or-live-busyness"
- "scrape-menus-services-or-booking-links"
- "geocode-an-address-or-reverse-geocode-a-coordinate"
- "email-call-or-message-a-business (delivery writes to YOUR tools only)"
- "scrape-a-single-place-by-url-or-place-id (query-driven only)"
search_keywords:
# First entry is the H1 / meta-description target. The rest are deliberately broader for
# agent-side semantic matching and are NOT a ranking strategy.
- "google maps scraper"
- "google maps business scraper"
- "places api alternative"
- "local business lead generation"
- "extract phone numbers from google maps"
- "google maps data export"
- "local seo competitor data"
- "business listings by city"
- "gps coordinates scraper"
- "mcp google maps tool"
input_entities:
- "searchQueries (REQUIRED, array of free-text Maps searches; each runs separately)"
- "maxResults (1-500 PER QUERY; Google itself stops feeding at roughly 120)"
- "language (en | hi | fr | de | es - changes category names and hours labels)"
- "batchSize (1-20 place pages opened at once; 5 is the safe default; hidden in Console, settable via API/SDK/MCP)"
max_results_is_per_query_and_google_caps_it: >
maxResults applies to EACH query independently, so three queries at 100 is up to 300 rows.
The schema allows up to 500, but Google Maps stops feeding a search at roughly 120 places, so
values far above that will not return more - split the area into narrower queries instead.
rating_and_review_count_are_strings: >
CRITICAL: rating and reviewCount are captured as DISPLAYED TEXT, not numbers. reviewCount can
arrive as "1,234", "(1,234)" or "1,234 reviews" depending on which element Maps rendered.
Parse both before sorting or averaging - a naive numeric sort will be wrong.
language_changes_the_data_not_just_the_ui: >
language sets the Maps interface locale, so category names, opening-hours labels and tag
wording come back in that language. Mixing languages across runs breaks any grouping on
`category` - pick one language per dataset.
no_proxy_input: >
This Actor exposes no proxyConfiguration; it runs from Apify's own IPs. If a large run slows
down or returns thin results, split the search into smaller queries; API/SDK/MCP callers can also
proxy setting.
every_row_is_attributed: >
searchQuery carries the exact query that produced the row, so a multi-query run stays
separable. There is NO de-duplication across queries: a business matching two searches is
written twice, once per query. Dedupe on url or plusCode if you need a unique list.
output_entities:
[
"name",
"category",
"rating",
"reviewCount",
"address",
"phone",
"website",
"hours",
"tags",
"latitude",
"longitude",
"imageUrl",
"plusCode",
"url",
"searchQuery",
]
hours_is_one_string: >
hours is a single pipe-joined string of the weekly table ("Monday 9 am-5 pm | Tuesday ..."),
not a structured object. Split on " | " if you need per-day values.
tags_is_null_not_empty: >
tags is an array when the listing has attribute chips and NULL when it has none - not an empty
array. Guard for null before iterating.
rows_are_saved_as_they_are_scraped: >
Each business is pushed the moment its page is parsed, so an aborted or timed-out run keeps
everything collected up to that point.
failures_are_isolated_per_place: >
A place page that times out or fails to parse is logged and skipped; the batch and the run
continue. A query whose feed never loads logs "Feed not found" and contributes nothing.
known_limitations:
no_review_text: "rating and review COUNT only - no reviews, reviewers or replies"
no_popular_times: "live busyness and popular-times histograms are not collected"
no_menus_or_services: "menus, service lists and booking links are out of scope"
contact_fields_often_missing: "many listings publish no phone or no website; those fields are null"
coordinates_from_url: "latitude/longitude are parsed from the resolved Maps URL and can be null"
no_place_id: "Google's place_id is not exposed; use url or plusCode as the key"
ratings_are_strings: "see rating_and_review_count_are_strings"
free_plan_capped: "free Apify plans stop at 5 places per query"
pricing:
model: "usage-based"
billing_unit: "Apify platform usage consumed by the run"
hard_ceiling_parameter: "maxTotalChargeUsd"
note: "Browser-driven: one feed scroll per query plus one page load per business. Cost tracks the number of businesses. Current rates are on the Actor's Apify Store page."
when_to_use: >
You want local business data as rows - a prospect list for a category and city, contact
details for outreach you will run yourself, competitive density and rating analysis, or
coordinates for mapping.
when_not_to_use: >
You need review text, popular times, menus, a place lookup by ID or URL, geocoding, or a tool
that contacts businesses for you.
mcp_delivery:
modes: ["perPlace", "chunked", "summary", "none"]
default: "perPlace"
per_place_is_the_default: >
deliveryMode defaults to perPlace - one connector call per business, which is what you want
for a CRM row each, and uncapped. A 300-place run is 300 calls; use summary or chunked for a
digest.
per_place_placeholders:
[
"{name}",
"{category}",
"{rating}",
"{reviewCount}",
"{address}",
"{phone}",
"{website}",
"{hours}",
"{tags}",
"{latitude}",
"{longitude}",
"{coordinates}",
"{imageUrl}",
"{plusCode}",
"{url}",
"{googleMapsUrl}",
"{searchQuery}",
"{place}",
"{message}",
]
summary_and_chunked_placeholders:
[
"{placeCount}",
"{queries}",
"{language}",
"{withPhone}",
"{withWebsite}",
"{places}",
"{message}",
]
chunked_only_placeholders: ["{part}", "{partCount}"]
the_log_prints_the_tools_argument_schema: >
With a valid mcpTool set, the run log prints "Connector [tool] expects arguments matching
this schema:" followed by that connector's own JSON Schema. Use it to get mcpArguments right
on the second attempt instead of guessing.
delivery_writes_to_your_tools_only: >
Delivery pushes DATA into connectors you authorised. It never contacts a business.
never_blocks_the_dataset: >
Delivery runs after the dataset is saved. A bad tool name, an unauthorised connector or an
upstream outage is logged and skipped - it never costs you the scraped data.
alternatives:
- "techforce.global/google-maps-leads-sales-intelligence-tool for sales-intelligence enrichment on Maps leads"
- "techforce.global/finance-google-maps-lead-scraper for finance and professional services"
- "techforce.global/construction-lead-intelligence-scraper for construction and trades"
- "techforce.global/google-maps-healthcare-leads-sales-intelligence-tool for healthcare providers"
- "techforce.global/google-maps-hospitality-lead-scraper for hotels, restaurants and hospitality"

{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "Google Maps Scraper",
"alternateName": "Google Maps Scraper β€” Business Listings, Contacts & Coordinates",
"applicationCategory": "BusinessApplication",
"operatingSystem": "Cloud (Apify platform)",
"description": "Runs any Google Maps search β€” one query or many β€” and returns one row per business: name, category, rating, review count, address, phone, website, opening hours, attribute tags, latitude and longitude, Plus Code, listing photo, the Maps URL and the query that produced the row. No Google account, Places API key or billing project is required. Five interface languages, configurable page concurrency, and optional delivery into Airtable, Notion, Slack, Google Sheets, HubSpot or any MCP connector β€” one call per business for CRM-ready records.",
"url": "https://apify.com/techforce.global/google-maps-scraper",
"offers": {
"@type": "Offer",
"priceCurrency": "USD",
"category": "Pay per platform usage",
"description": "Usage-based: billed for the compute time and resources a run consumes, not per row. Browser-driven, so cost tracks the number of businesses scraped. Current rates are shown on the Actor's Apify Store page."
}
}

What this Actor does

  • Takes the search you'd actually type. "dentists in Ahmedabad",

    "vegan restaurants near Shoreditch"
    , "HVAC contractors Dallas" β€” free text, exactly as Maps interprets it. Pass a list and each query runs separately.

  • Returns the contact fields that matter for outreach. Phone, website and address alongside name and category β€” plus a Contacts only dataset view that is already CRM-shaped.

  • Gives you the geo data too. latitude, longitude and plusCode, so the same run feeds a map, a territory plan or a radius calculation without a second lookup.

  • Stamps every row with its query. searchQuery means a fifty-query sweep across a region is still separable afterwards β€” no bookkeeping on your side.

  • Scrolls the results feed like a person would. It keeps loading until your cap is reached or Maps stops producing new places, then opens each listing to read its detail panel.

  • Five interface languages. en, hi, fr, de, es β€” which changes the data, not just the UI: category names and opening-hours labels come back in that language.

  • Saves as it goes. Each business is written the moment its page is parsed, so an aborted or timed-out run keeps everything collected so far.

  • Survives bad listings. A place page that times out is logged and skipped; the batch and the run carry on.

  • Delivers one CRM record per business. perPlace is the default delivery mode for exactly that reason β€” Airtable rows, Notion pages, Sheets rows, HubSpot records. Delivery writes to your tools; it never contacts a business.


When to use it β€” and when not to

ScenarioFit
A prospect list for a category and cityβœ… Strong β€” the core use case
Phone numbers and websites for outreach you run yourselfβœ… Strong β€” plus a Contacts-only view
Coordinates for mapping or territory planningβœ… Strong β€” latitude, longitude, plusCode
Competitive density across neighbourhoodsβœ… Strong β€” many queries, each row attributed
Rating and review-volume comparisonβœ… Strong β€” but both fields are strings, parse first
Opening hours for a whole categoryβœ… Strong β€” one pipe-joined string per business
Pushing one CRM record per businessβœ… Strong β€” perPlace delivery
Multi-language marketsβœ… Strong β€” but one language per dataset
More than ~120 results for one search⚠️ Google stops feeding; split into narrower queries
A unique business list across queries⚠️ No dedup β€” dedupe on url or plusCode
Guaranteed contact details⚠️ Many listings publish no phone or website; those are null
Review text or reviewer identities❌ Rating and review count only
Popular times / live busyness❌ Not collected
Menus, services or booking links❌ Out of scope
A specific place by URL or place ID❌ Query-driven only; no place_id is exposed
Geocoding an address you already have❌ Wrong tool β€” use a geocoding API
Emailing or calling the businesses❌ Read-only; delivery writes to your tools only

⚑ Execution Rules (Avoid Common Agent Failures)

  1. searchQueries is required and is an array. Each query runs separately and is capped independently by maxResults, so three queries at 100 is up to 300 rows β€” not 100.

  2. maxResults is per query, and Google caps you long before 500. The schema accepts up to 500, but Maps itself stops feeding a search at roughly 120 places. Asking for 500 doesn't get you 500 β€” split the area into narrower queries instead ("dentists in Camden" beats "dentists in London" for coverage).

  3. rating and reviewCount are strings, not numbers. They're captured as displayed, so reviewCount can be "1,234", "(1,234)" or "1,234 reviews" depending on what Maps rendered. Parse both before sorting or averaging.

  4. language changes the data, not just the interface. Category names, hours labels and tag wording all come back in the chosen language. Use one language per dataset or grouping on category becomes meaningless.

  5. There is no proxy input. The Actor runs from Apify's IPs. If a big run slows or thins out, split the query into smaller searches. API, SDK and MCP callers can also lower batchSize;

  6. batchSize is a rate-limit dial, not just a speed dial, and it is API-only. Default 5, range 1–20, hidden in the Console form. Higher opens more place pages at once β€” faster, more memory, more likely to trip Maps' limits. Raise it only when runs are slow and completing cleanly.

  7. No de-duplication across queries. A business matching two searches is written twice, once per query, each row tagged in searchQuery. Dedupe on url or plusCode for a unique list.

  8. tags is null, not [], when a listing has no attribute chips. Guard before iterating.

  9. hours is one pipe-joined string, e.g. "Monday 9 am–5 pm | Tuesday 9 am–5 pm | …". Split on " | " if you need per-day values.

  10. phone and website are frequently null. Plenty of listings publish neither. Filter for what you need rather than assuming every row is contactable.

  11. latitude/longitude come from the resolved Maps URL and can be null when it didn't carry coordinates. plusCode is the more reliable location key when present.

  12. There is no place_id. Use url (or plusCode) as your join key across runs.

  13. deliveryMode defaults to perPlace β€” one connector call per business, uncapped. That's right for CRM rows and wrong for a Slack channel; use summary or chunked for digests.

  14. Free Apify plans stop at 5 places per query. A smoke test, not a prospect list.

dependencies:
- rule: "searchQueries is required and must contain at least one query"
on_violation: "nothing is searched; the run finishes with an empty dataset"
- rule: "maxResults applies per query, and Google Maps stops at roughly 120 per search"
note: "split the area into narrower queries rather than raising the cap"
- rule: "rating and reviewCount are display strings"
on_violation: "numeric sorts and averages are silently wrong"
- rule: "one language per dataset"
on_violation: "category names arrive in mixed languages and grouping breaks"
- rule: "delivery requires mcpConnector AND mcpTool"
on_violation: "delivery is skipped with a warning; every dataset row is still written"
- rule: "mcpArguments must match the connector tool's real argument schema"
on_violation: "the calls fail; the log prints the expected schema so you can fix it"
authentication:
actor_input: none
platform: "Apify API token required for API / SDK / MCP invocation"
target_site: "no Google account, Places API key or billing project required (public listings only)"

πŸ“₯ Input Contract

.actor/input_schema.json is authoritative β€” if this page disagrees with the schema files, the schema files win.

ParameterTypeRequiredDefaultNotes
searchQueriesarray<string>βœ… Yes["restaurants in Mumbai"]Free-text Maps searches. Each runs separately and is capped independently
maxResultsinteger⬜ No201–500 per query. Maps itself stops at roughly 120
languagestring (enum)⬜ Noenen Β· hi Β· fr Β· de Β· es β€” changes category and hours wording
batchSizeinteger⬜ No51–20 place pages opened at once. Higher = faster, and more rate-limit risk. Hidden in Console, settable via API / SDK / MCP

Writing good queries. The Actor passes your text to Maps unchanged, so query craft is the main quality lever. "dentists in Camden" returns a fuller, more relevant set than "dentists in London" β€” because Maps stops feeding any single search at roughly 120 places, a handful of neighbourhood queries beats one city-wide query every time.

Delivery (optional)

ParameterTypeDefaultDescription
mcpConnectorstring""A connector you've authorised on your Apify account β€” Airtable, Notion, Slack, Google Sheets, HubSpot, or any MCP-compatible connector. Leave empty for dataset-only runs.
deliveryModestring (enum)perPlaceperPlace (one call per business β€” best for a CRM row each) Β· chunked (split a long list) Β· summary (one call, every place) Β· none.
mcpToolstring""Tool to call β€” create_record, create_page, send_message, append_row. Required when a connector is set.
mcpArgumentsobject{}Arguments for the tool. String values support {placeholder} tokens.
mcpMessageTemplatestring""Template rendered and exposed to mcpArguments as {message}.

Placeholders by mode:

ModePlaceholders
perPlaceEvery field β€” {name} {category} {rating} {reviewCount} {address} {phone} {website} {hours} {tags} {latitude} {longitude} {imageUrl} {plusCode} {url} {searchQuery} β€” plus {coordinates} ("lat,lng"), {googleMapsUrl} (alias for url), {place} (a formatted multi-line block) and {message}
summary{placeCount} {queries} {language} {withPhone} {withWebsite} {places} {message}
chunkedthe same as summary, plus {part} {partCount} β€” {places} holds one part

{withPhone} and {withWebsite} count how many rows actually carry those fields β€” handy for a one-line quality check in a digest. Nulls render as an empty string.

Don't guess mcpArguments. Run once with mcpConnector and mcpTool set, then read the log line Connector [tool] expects arguments matching this schema: β€” it prints that connector's own JSON Schema. A mismatched shape fails only the delivery calls; the dataset is already saved.


πŸ“€ Output Contract & Data Structure

One dataset row per business per query, written as each listing is parsed.

FieldTypeNullableMeaning
namestringyesBusiness name as shown on the listing
categorystringyesPrimary Maps category β€” in the chosen language
ratingstringyesAverage rating as displayed text, e.g. "4.5". Null when unrated
reviewCountstringyesReview count as displayed text β€” "1,234", "(1,234)" or "1,234 reviews"
addressstringyesFull address line
phonestringyesListed phone number. Frequently null
websitestringyesListed website. Frequently null
hoursstringyesThe weekly table as one pipe-joined string
tagsarrayyesAttribute chips. null β€” not [] β€” when the listing has none
latitudenumberyesParsed from the resolved Maps URL; null when absent
longitudenumberyesAs above
imageUrlstringyesListing photo URL
plusCodestringyesGoogle Plus Code β€” a stable location key when present
urlstringnoThe resolved Maps URL. Use this as the join / dedup key
searchQuerystringnoThe query from your input that produced this row

The two fields that mislead: rating and reviewCount are text, so "4.5" and "(1,234)" sort as strings. Parse them before any numeric work. And tags is null rather than an empty array β€” a for loop over it will throw.

Example output object

{
"name": "Bombay Dental Studio",
"category": "Dental clinic",
"rating": "4.7",
"reviewCount": "1,284",
"address": "12 Linking Road, Bandra West, Mumbai, Maharashtra 400050",
"phone": "+91 22 2640 0000",
"website": "https://example-dental.in/",
"hours": "Monday 9 am–8 pm | Tuesday 9 am–8 pm | Wednesday 9 am–8 pm | Thursday 9 am–8 pm | Friday 9 am–8 pm | Saturday 10 am–6 pm | Sunday Closed",
"tags": ["Wheelchair accessible entrance", "Appointments recommended"],
"latitude": 19.0607,
"longitude": 72.8362,
"imageUrl": "https://lh3.googleusercontent.com/p/…",
"plusCode": "3JMV+4C Mumbai, Maharashtra",
"url": "https://www.google.com/maps/place/…/@19.0607,72.8362,17z/…",
"searchQuery": "dentists in Mumbai"
}

Values illustrate shape and formatting β€” the business is invented. A realistic run mixes rows like this with ones where phone, website or tags are null; that's the listing data, not a defect.

What a successful run looks like

  • Status: SUCCEEDED
  • Dataset: one row per business per query, up to maxResults for each
  • Log: πŸ” Query: … | Language: …, then ↳ URLs found: N as the feed scrolls, then πŸš€ Scraping N places in batches of B…, a βœ… line per business, and a final πŸ“¦ N place(s) saved across M query/queries.
  • Zero results is also a success. A query whose feed never loads logs Feed not found for '<query>'; one with no matches logs No URLs collected. Either way the run ends SUCCEEDED with nothing for that query.
  • ⚠️ No data extracted or ❌ lines mean individual listings were skipped β€” the rest of the batch is unaffected.
  • Fewer rows than maxResults is normal β€” Maps stops feeding around 120 per search, and niche queries run dry much sooner.

Dataset views

ViewShows
OverviewName, category, rating, reviews, address, phone, website, hours, source query, Maps link
Contacts onlyCRM-ready columns: name, category, phone, website, address, Maps link
Map & mediaListing photo, name, latitude, longitude, Plus Code, tags, Maps link

▢️ Invocation & Integration

Authenticate every call with Authorization: Bearer <APIFY_TOKEN>.

Apify Console

Open the Actor page β†’ Input tab β†’ add your search queries β†’ set maxResults per query β†’ pick a language β†’ Start.

Model Context Protocol (MCP)

claude mcp add --transport http apify \
"https://mcp.apify.com?tools=techforce.global/google-maps-scraper"

Then ask your agent for "dentists in Ahmedabad with a website and 4+ stars".

Python (apify-client)

import os
import re
from collections import Counter
from apify_client import ApifyClient
client = ApifyClient(os.getenv("APIFY_TOKEN"))
run = client.actor("techforce.global/google-maps-scraper").call(run_input={
"searchQueries": ["dentists in Bandra", "dentists in Andheri"],
"maxResults": 60, # per query β€” Maps stops feeding around 120
"language": "en",
"batchSize": 5,
"deliveryMode": "none",
})
places = list(client.dataset(run["defaultDatasetId"]).iterate_items())
if not places:
raise SystemExit("Nothing scraped β€” check the log for 'Feed not found'.")
print(Counter(p["searchQuery"] for p in places))
# rating and reviewCount are STRINGS as displayed β€” parse before any numeric work.
def num(value):
if not value:
return None
match = re.search(r"[\d.,]+", str(value))
return float(match.group(0).replace(",", "")) if match else None
rated = [(num(p["rating"]), num(p["reviewCount"]), p) for p in places]
strong = [(r, c, p) for r, c, p in rated if r and r >= 4.0 and c and c >= 50]
print(f"{len(strong)}/{len(places)} rated 4.0+ with 50+ reviews")
# Contactable prospects only β€” phone and website are frequently null.
contactable = [p for p in places if p.get("phone") or p.get("website")]
print(f"{len(contactable)}/{len(places)} have a phone or a website")
# No dedup across queries β€” the same clinic can appear in both.
unique = {p["url"]: p for p in places}
print(f"{len(unique)} unique businesses from {len(places)} rows")

JavaScript / TypeScript (apify-client)

import { ApifyClient } from "apify-client";
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor("techforce.global/google-maps-scraper").call({
searchQueries: ["coffee shops in Shoreditch"],
maxResults: 80,
language: "en",
deliveryMode: "none",
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
// tags is null (not []) when the listing has no attribute chips.
const accessible = items.filter((p) =>
(p.tags ?? []).some((t: string) => t.toLowerCase().includes("wheelchair")),
);
console.log(`${accessible.length} listings mention wheelchair access`);
// hours is one pipe-joined string β€” split it if you need per-day values.
for (const p of items.slice(0, 5)) {
const days = (p.hours ?? "").split(" | ").filter(Boolean);
console.log(`${p.name}: ${days.length} day rows, ${p.phone ?? "no phone"}`);
}
// Coordinates come from the resolved URL and can be null.
const mappable = items.filter(
(p) => p.latitude !== null && p.longitude !== null,
);
console.log(`${mappable.length}/${items.length} rows are mappable`);

cURL β€” synchronous (waits, returns the dataset)

curl -X POST \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <YOUR_APIFY_TOKEN>' \
-d '{ "searchQueries": ["plumbers in Leeds"], "maxResults": 20, "language": "en", "deliveryMode": "none" }' \
'https://api.apify.com/v2/acts/techforce.global~google-maps-scraper/run-sync-get-dataset-items'
# 1. Start with a hard spend limit
curl -X POST -H 'Content-Type: application/json' \
-H 'Authorization: Bearer <YOUR_APIFY_TOKEN>' \
-d '{ "searchQueries": ["gyms in Camden","gyms in Islington","gyms in Hackney"],
"maxResults": 100, "batchSize": 5 }' \
'https://api.apify.com/v2/acts/techforce.global~google-maps-scraper/runs?maxTotalChargeUsd=10'
# 2. Poll, then 3. fetch the dataset when SUCCEEDED
curl -H 'Authorization: Bearer <YOUR_APIFY_TOKEN>' 'https://api.apify.com/v2/actor-runs/<runId>'
curl -H 'Authorization: Bearer <YOUR_APIFY_TOKEN>' \
'https://api.apify.com/v2/datasets/<DATASET_ID>/items?clean=true&format=json'

Covering a city properly

{
"searchQueries": [
"dentists in Bandra Mumbai",
"dentists in Andheri Mumbai",
"dentists in Powai Mumbai",
"dentists in Colaba Mumbai"
],
"maxResults": 100,
"language": "en",
"batchSize": 5,
"deliveryMode": "none"
}

Four neighbourhood queries beat one "dentists in Mumbai" query, because Maps stops feeding any single search at roughly 120 places. Dedupe on url afterwards β€” the boundaries overlap.

One Airtable record per business

{
"searchQueries": ["HVAC contractors in Dallas"],
"maxResults": 100,
"mcpConnector": "<your-authorized-airtable-connector>",
"mcpTool": "create_record",
"deliveryMode": "perPlace",
"mcpArguments": {
"fields": {
"Name": "{name}",
"Category": "{category}",
"Phone": "{phone}",
"Website": "{website}",
"Address": "{address}",
"Rating": "{rating}",
"Maps": "{url}",
"Source query": "{searchQuery}"
}
}
}

Match the field names to your actual Airtable columns. If the call is rejected, the log prints the schema that connector really expects β€” fix mcpArguments from that.

A digest into Slack instead

{
"searchQueries": ["coworking spaces in Berlin"],
"maxResults": 50,
"language": "de",
"mcpConnector": "<your-authorized-slack-connector>",
"mcpTool": "send_message",
"deliveryMode": "summary",
"mcpMessageTemplate": "{placeCount} places for {queries} β€” {withPhone} with a phone number:\n\n{places}",
"mcpArguments": { "channel": "#prospecting", "text": "{message}" }
}

Set summary deliberately β€” perPlace is the default and would post one Slack message per business.


⚠️ Autonomous Agent Error Handling & Resolution Matrix

Read this first: nothing about the listings fails the run. A query whose feed never loads, a listing that won't parse, and a search with no matches are all logged while the run continues to SUCCEEDED. An agent that checks run status alone will read an empty dataset as success-with-data β€” check the row count and the log.

The CONDITION labels are documentation handles for observable states; this Actor writes no error code into the dataset.

ConditionWhat you observeCauseDeterministic agent action
NO_QUERIESEmpty dataset, no πŸ” Query: linessearchQueries emptyModify Input: supply at least one query.
FEED_NOT_FOUNDLog: Feed not found for a query; that query yields nothingMaps did not render a results feed β€” a blocked, consent-walled or malformed searchRetry, and simplify the query wording. If it repeats across queries, lower batchSize from an API/SDK/MCP call.
NO_URLS_COLLECTEDLog: No URLs collected for '<query>'The search genuinely has no placesModify Input: broaden or rephrase the query.
PLACE_SKIPPEDLog: ⚠️ No data extracted or a ❌ line for one placeThat listing timed out or didn't render the detail panelExpected, partial. The rest of the batch is unaffected.
FEWER_THAN_MAXRESULTSRows well below maxResultsMaps stops feeding around 120 per search, and niche queries dry up soonerModify Input: split into narrower geographic queries.
RATING_SORTS_WRONG"4.5" ordering above "4.9", or averages failingrating / reviewCount are display stringsFix consumer: parse to numbers first.
TAGS_ITERATION_ERRORConsumer throws iterating tagsIt is null, not [], when the listing has no chipsFix consumer: guard for null.
MIXED_LANGUAGE_CATEGORIEScategory values in two languages across runslanguage changes the returned wordingFix pipeline: one language per dataset.
NULL_CONTACTSMany rows with no phone or websiteThose listings publish neitherExpected. Filter for contactable rows.
NULL_COORDINATESlatitude / longitude nullThe resolved URL carried no @lat,lngExpected. Fall back to plusCode or address.
DUPLICATE_BUSINESSESThe same business twiceIt matched two queries; each row is taggedExpected. Dedupe on url or plusCode.
NO_PLACE_IDYou wanted Google's place_idIt is not exposedFix expectation: use url or plusCode as the key.
RUN_SLOW_OR_THINLong run, or results thinning out mid-runbatchSize too high for Maps tolerance β€” there is no proxy settingModify Input: split the query into smaller searches; from an API/SDK/MCP call also drop batchSize to 2–3.
FREE_PLAN_CAPPED5 places per query; log: Free plan detected β€” capping results to 5 per query.Free Apify planUpgrade the plan.
CONNECTOR_CALL_STORMHundreds of connector callsdeliveryMode defaults to perPlace, uncappedModify Input: switch to summary or chunked.
DELIVERY_ARGUMENT_MISMATCHEvery connector call is rejectedmcpArguments doesn't match the tool's schemaRead the log: Connector [tool] expects arguments matching this schema: prints it. Dataset unaffected.
CONNECTOR_TOOL_NOT_FOUNDLog: Tool 'x' is not available on this connector. Available tools: …Wrong mcpTool name (empty is logged separately)Modify Input: copy an exact name from that log line.
CONNECTOR_UNREACHABLELog: Could not reach the connector: …Connector not authorised, or running locallyAuthorise it, or set deliveryMode: "none". Data already saved.
AUTH_INVALIDHTTP 401Missing or malformed APIFY_TOKENAbort & prompt user for a valid token.
RATE_LIMITED_APIFYHTTP 429Apify API rate / concurrency limits β€” not GoogleRetry: exponential backoff (2s, 4s, 8s).
SYNC_TIMEOUTHTTP 408 on the sync endpointRun exceeded the synchronous ceilingReconfigure: async POST /runs + polling.

Provenance: every log line, status and dataset condition above is taken from this Actor's own source and verified against it. The three HTTP rows describe the Apify API transport layer, not this Actor β€” confirm them against the Apify API reference before wiring them into automated retry logic.


πŸ—£οΈ Natural Language β†’ Actor Mapping

| A user says… | Intent | Constructed Actor input | | --------------------------------------- | ---------------- | ---------------------------------------------------------------------------- | --- | | "Find dentists in Mumbai" | Core search | {"searchQueries":["dentists in Mumbai"],"maxResults":50} | | "Get me every gym in these three areas" | Multi-area sweep | One query per area, then dedupe on url | | "I need their phone numbers" | Contact list | Scrape, then filter rows where phone is non-null | | "Only businesses with a website" | Qualified list | Filter website non-null | | "Highest-rated restaurants nearby" | Ranking | Scrape, parse rating to a number, then sort | | "Which have lots of reviews?" | Popularity | Parse reviewCount to a number; treat as a proxy for footfall | | "Where are they on a map?" | Geo | Read latitude/longitude, fall back to plusCode | | "What are their opening hours?" | Hours | Read hours, split on " | " | | "Build me a prospect list in Airtable" | Scrape + CRM | Add mcpConnector, mcpTool: "create_record", deliveryMode: "perPlace" | | "Results in German" | Localised run | {"language":"de"} β€” and keep the whole dataset in one language | | "Give me 500 results for one city" | Volume | ⚠️ Maps stops near 120 β€” split into neighbourhood queries | | "Just a quick sample" | Cheap probe | {"searchQueries":["<query>"],"maxResults":5} | | "It returned nothing" | Empty result | Check the log for Feed not found / No URLs collected; simplify the query | | "What do the reviews say?" | Review text | ❌ Not collected β€” rating and count only | | "When is it busiest?" | Popular times | ❌ Not collected | | "Look up this place ID" | Place lookup | ❌ Query-driven only; no place_id is exposed | | "Email all of them for me" | Outreach | ❌ Read-only β€” delivery writes to your tools, never to the business |

Should NOT route here: review text, reviewers or replies Β· popular times / live busyness Β· menus, services or booking links Β· a single place by URL or place ID Β· geocoding an address you already hold Β· contacting businesses on your behalf Β· non-Maps sources. For a sector-specific lead build, prefer the vertical siblings.


🧭 Agent Execution & Routing Logic

Does the user want GOOGLE MAPS BUSINESS LISTINGS?
β”œβ”€β”€ NO β†’ wrong Actor.
β”‚ review text / popular times / menus β†’ not collected anywhere here
β”‚ geocoding a known address β†’ a geocoding API
β”‚
β”œβ”€β”€ YES, but for ONE INDUSTRY with enrichment?
β”‚ finance β†’ Finance Google Maps Lead Scraper
β”‚ construction β†’ Construction Lead Intelligence Scraper
β”‚ healthcare β†’ Healthcare Leads Sales Intelligence Tool
β”‚ hospitality β†’ Hospitality Lead Scraper
β”‚ general sales intelligence β†’ Google Maps Leads Sales Intelligence Tool
β”‚
└── YES, any category, full query control
β”‚
β”œβ”€β”€ searchQueries β€” REQUIRED, an ARRAY of free-text Maps searches
β”‚ want a whole city? β†’ SPLIT INTO NEIGHBOURHOOD QUERIES
β”‚ (Maps stops feeding one search at roughly 120 places)
β”‚
β”œβ”€β”€ maxResults β†’ PER QUERY (1-500, but ~120 is the real ceiling)
β”œβ”€β”€ language β†’ en/hi/fr/de/es; CHANGES category + hours wording.
β”‚ ONE LANGUAGE PER DATASET.
β”œβ”€β”€ batchSize β†’ 1-20, default 5. A RATE-LIMIT dial. API/SDK/MCP ONLY.
β”‚ Runs thinning out? Split the query; API callers can LOWER it.
β”‚
β”œβ”€β”€ What is the actual question?
β”‚ β”œβ”€β”€ contactable list β†’ filter phone / website non-null
β”‚ β”œβ”€β”€ best rated β†’ PARSE rating to a number first (it is a string)
β”‚ β”œβ”€β”€ busiest β†’ PARSE reviewCount (string, may contain commas/brackets)
β”‚ β”œβ”€β”€ mapping β†’ latitude/longitude, fall back to plusCode
β”‚ β”œβ”€β”€ hours β†’ split `hours` on " | "
β”‚ └── unique list β†’ dedupe on url or plusCode (no dedup across queries)
β”‚
└── Should results land in another app?
β”œβ”€β”€ yes β†’ mcpConnector + mcpTool, then READ THE LOG's printed argument schema
β”‚ CRM row each β†’ perPlace (DEFAULT, uncapped)
β”‚ digest β†’ summary Β· long list β†’ chunked
└── no β†’ deliveryMode "none"
[Execute Actor]
β”œβ”€β”€β–Ί SUCCEEDED with rows β†’ dedupe, parse the string numerics, then use
β”œβ”€β”€β–Ί SUCCEEDED, 0 rows β†’ READ THE LOG: Feed not found / No URLs collected
└──► 5 rows per query β†’ free plan cap, not a prospect list

πŸ’° Cost Control & Pricing Transparency

Billing model: usage-based. This Actor bills for platform usage β€” the compute time and resources a run consumes β€” not per row. Current rates are on the Actor's Apify Store page; the Console shows the live total mid-run.

The shape to keep in mind: one feed scroll per query, then one browser page load per business. Cost tracks the number of businesses, so maxResults Γ— number of queries is your bill.

ChoiceEffect on cost
maxResultsThe main lever β€” it is the number of place pages opened, per query.
Number of queriesMultiplies it: four queries at 100 is up to 400 page loads.
batchSizeDoesn't change the work, only how fast it's done β€” but too high causes retries and thinning. API-only.
languageNo cost effect.
Delivery modeperPlace adds one connector call per business; summary adds one.

Worked examples β€” relative run shape

ScenarioQueriesPage loadsRowsNotes
Probe β€” 1 query, maxResults: 51~6≀ 5Baseline. Confirms the query returns places.
Standard β€” 1 query, 50 places1~51≀ 50The common prospecting run.
City coverage β€” 4 neighbourhoods Γ— 1004~404≀ 400The right shape for real coverage.
Over-ask β€” 1 query, maxResults: 5001~121~120Wasted intent: Maps stops feeding; you get ~120.
Regional sweep β€” 20 queries Γ— 10020~2,020≀ 2,000Set maxTotalChargeUsd.

Keeping runs predictable:

  1. First run: one query, maxResults: 5. Confirms the query wording returns the right places.
  2. Split geography rather than raising maxResults β€” past ~120 the higher number buys nothing.
  3. batchSize stays at 5 unless you call the API β€” the field is hidden in the Console form. Lower it from an API/SDK/MCP call if results thin out.
  4. Always set maxTotalChargeUsd on multi-query sweeps.
  5. Filter for contactable rows after the fact β€” you can't ask Maps for "only listings with a phone", so budget for the ones without.
  6. Free Apify plans get 5 places per query β€” a connectivity check, not a prospect list.

πŸ› οΈ Troubleshooting

Most problems are query craft, the two string-typed numeric fields, or asking for more than Maps will feed β€” rarely a scraper fault.

#Symptom you seeMost likely causeFix
1Empty datasetNo queries supplied, or the feed never loadedCheck the log for πŸ” Query: lines and Feed not found for ….
2Log: Feed not found for a queryMaps did not render a results feed for that searchSimplify the wording and retry. If it happens across queries, lower batchSize from an API/SDK/MCP call.
3Log: No URLs collected for '<query>'The search genuinely has no placesBroaden or rephrase β€” check the same query on maps.google.com.
4Asked for 500, got ~120Google stops feeding a single search at roughly 120 placesSplit into neighbourhood or category-narrowed queries.
5Rating sort is nonsenserating is a stringParse to a number before sorting or averaging.
6reviewCount won't parseIt's display text β€” "1,234", "(1,234)" or "1,234 reviews"Strip non-digits, then parse.
7Consumer throws on tagsIt's null, not [], when there are no chipsGuard for null before iterating.
8Categories in two languagesDifferent language values across runsOne language per dataset.
9Half the rows have no phoneMany listings publish noneExpected β€” filter for contactable rows.
10latitude/longitude nullThe resolved URL carried no coordinatesFall back to plusCode or geocode address yourself.
11No place_id anywhereIt isn't exposedUse url or plusCode as your key.
12The same business appears twiceIt matched two queries; each row is taggedDedupe on url or plusCode.
13Run is slow, or results thin out mid-runbatchSize too high β€” there is no proxy input to tuneSplit the query into smaller searches. From an API/SDK/MCP call you can also drop batchSize to 2–3.
14⚠️ No data extracted on some placesThose listings didn't render a detail panel in timePartial by design; re-run for the gaps if they matter.
15Only 5 places per queryFree plan capUpgrade; the log says Free plan detected β€” capping results to 5 per query.
16Hundreds of Slack messagesdeliveryMode defaults to perPlaceUse summary or chunked for channels; keep perPlace for CRMs.
17Every connector call rejectedmcpArguments doesn't match the tool's schemaRead Connector [tool] expects arguments matching this schema: in the log and copy that shape.
18Airtable / Notion received nothingDelivery needs both mcpConnector and mcpToolSet both. Every dataset row is still saved.
19HTTP 401 / 403 from the Apify APIMissing, expired or malformed APIFY_TOKENRegenerate in Console β†’ Settings β†’ API & Integrations.
20HTTP 429 / 408 from the Apify APIApify rate limits / the sync endpoint's time ceilingBack off (2s β†’ 4s β†’ 8s), or switch to async POST /runs + polling.

Diagnostic checklist before opening an issue

  1. Run the same query on maps.google.com β€” if it returns little there, the Actor can't do better.
  2. Read the ↳ URLs found: lines β€” they tell you whether the problem is discovery or extraction.
  3. Baseline run: one query, maxResults: 5, deliveryMode: "none".
  4. Check you're not fighting the ~120 ceiling before assuming results are missing.
  5. Confirm you parsed rating / reviewCount before trusting any ranking.
  6. Lower batchSize to 2 from an API/SDK/MCP call and re-run if listings are being skipped in bulk.

If the issue survives all six, open an Issues ticket (or email support) with the run ID, the input you used, and what you expected.


❓ FAQ

Do I need a Google account or a Places API key?

No. The Actor reads publicly visible Maps listings. You need an Apify account and API token; you need no Google credentials, API key or Cloud billing project.

Is this the official Google Places API?

No. This is an independent Actor and is not affiliated with, endorsed by, or sponsored by Google LLC. The Places API is the sanctioned route and worth using if its billing and caching terms suit you.

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 get more than about 120 results for a city?

You split the city. Google Maps stops feeding any single search at roughly 120 places no matter what maxResults says, so four neighbourhood queries return far more than one city-wide query. Dedupe on url afterwards, because the boundaries overlap.

Is maxResults per query or per run?

Per query. Three queries at 100 is up to 300 rows in one run.

Why does my rating sort come out wrong?

Because rating and reviewCount are captured as displayed text, not numbers β€” reviewCount can even arrive as "(1,234)" or "1,234 reviews". Strip the non-numeric characters and parse both before sorting or averaging.

Why is tags sometimes null instead of an empty array?

Because the extractor returns null when a listing has no attribute chips at all. Guard for null before iterating, or coalesce to [] at ingest.

What does the language setting actually change?

More than the interface. Category names, opening-hours labels and tag wording all come back in the chosen language, so category values differ between an en run and a de run. Keep one language per dataset or grouping breaks.

Can I use a proxy?

There’s no proxy input on this Actor β€” it runs from Apify’s own IPs. If a large run slows down or results thin out, split the search into smaller queries. API, SDK and MCP callers can also lower batchSize to 2–3; the field is hidden in the Console form.

What is batchSize for?

It’s how many place pages are opened at once. Default 5, range 1–20. Higher is faster but uses more memory and is more likely to trip Google’s rate limiting, so treat it as a throttle rather than a speed setting. It is hidden in the Console form and settable only via API, SDK or MCP.

Why do so many rows have no phone or website?

Because many businesses simply don't publish them on their listing. Both fields are null in that case β€” filter for contactable rows rather than assuming every result is reachable.

Is there a place_id?

No. Google's place_id isn't exposed. Use url as your join and de-duplication key, or plusCode where you need a location-stable identifier.

Why does the same business appear twice?

Because it matched two of your queries, and each row is tagged with its own searchQuery so per-query analysis stays honest. De-duplicate on url or plusCode for a unique list.

Can I get review text, popular times or menus?

No. This Actor returns the listing's own facts β€” identity, contact, location, hours, rating and review count. Review text, reviewers, popular times, menus and booking links are all out of scope.

Does it contact the businesses for me?

No, and it can't. MCP delivery pushes rows into connectors you authorised β€” Airtable, Notion, Slack, Sheets, HubSpot. Nothing is ever sent to a business.

Which delivery mode should I use?

perPlace (the default) when you want one CRM record per business β€” that's exactly what it's for. summary or chunked for a digest into a channel, since perPlace would post one message per business. none for dataset-only runs.

My connector calls all fail. What now?

Your mcpArguments shape doesn't match what the tool expects. Run once with the connector and tool set, then read the log line Connector [tool] expects arguments matching this schema: β€” it prints the connector's own JSON Schema. The dataset saves regardless, so a failed delivery costs you nothing but the retry.

Is the contact data personal data under GDPR?

Sometimes, yes. A switchboard number for a large firm generally isn't, but a sole trader's mobile and home address on their listing very much can be. Read Compliance & Data Privacy before you load it into an outreach tool.

What does it cost, and what do I get on the free plan?

Usage-based: you pay for the platform usage a run consumes, not per row. It's browser-driven β€” one page load per business β€” so the total number of businesses is the cost driver. Free Apify plans return 5 places per query. Pass maxTotalChargeUsd for a hard per-run ceiling.


πŸ” Companion machine-readable files

FileWhat it defines
.actor/input_schema.jsonEvery input, its type, default and bounds, plus the language and delivery enums
.actor/dataset_schema.jsonThe three Console views β€” Overview, Contacts only, Map & media
.actor/output_schema.jsonWhere the run's output is exposed
.actor/actor.jsonActor identity and storage wiring

If this page and the schema files disagree, the schema files win. They are what the platform validates against at run time; this page is documentation and can lag a release.

Note that dataset_schema.json defines views but no per-field JSON Schema, so the platform does not type-check pushed rows. The field table above is documentation of what the extractor produces, not a contract the platform enforces.


This is the general-purpose Maps scraper. The five siblings below are the same data source pre-aimed at one sector β€” worth checking before you build sector logic yourself.

ActorBest for
This ActorAny category, any query β€” full control over the search
Google Maps Leads β€” Sales Intelligence ToolSales-intelligence enrichment layered on Maps leads
Finance Google Maps Lead ScraperFinance and professional-services firms
Construction Lead Intelligence ScraperConstruction, trades and contractors
Google Maps Healthcare Leads β€” Sales Intelligence ToolHealthcare providers, clinics and practices
Google Maps Hospitality Lead ScraperHotels, restaurants and hospitality venues

Pick by intent: if your prospecting is confined to one of those sectors, start with the vertical Actor β€” check its page for the exact fields it adds on top of the listing data. If you need categories they don't cover, or you want to write the query yourself, use this one.

Pipeline patterns: territory build β€” one query per neighbourhood, dedupe on url, load into Airtable via perPlace Β· competitive density β€” the same category across many areas, then count rows and compare parsed ratings per area Β· map layer β€” latitude/longitude straight into your GIS or mapping tool Β· refresh cadence β€” the same queries on a schedule, joined on url, to catch new openings and closures.

Browse all Actors by Techforce Global at scraper.techforce.global.


πŸ” Compliance & Data Privacy

Trademark & affiliation. This is an independent Actor. It is not affiliated with, endorsed by, or sponsored by Google LLC. Google Mapsβ„’ is a trademark of Google LLC.

  • Public business listings only. Everything returned is visible to any signed-out visitor. No account login, no reviewer identities, no review text, no user photos, no private data.

  • ⚠️ "Business data" is not automatically "not personal data." A switchboard number for a hospital chain is corporate information. A sole trader's mobile number and home address on their Maps listing is personal data under GDPR / UK GDPR β€” and the same applies to a named practitioner's direct line. This Actor collects both kinds and cannot tell them apart. Under GDPR the obligations that follow β€” lawful basis, transparency, a route to object β€” sit with you.

  • Outreach is a separate legal question from collection. Cold email and cold calling are governed by PECR/ePrivacy in the UK and EU, CAN-SPAM in the US, CASL in Canada and equivalents elsewhere, and several of those distinguish sole traders from incorporated businesses. A phone number appearing on a public listing is not consent to be contacted, and in many jurisdictions you must also screen against a do-not-call register. Get advice before running outreach at scale.

  • The Actor never contacts anyone. MCP delivery writes into connectors you authorised. What you then do with the rows is entirely your decision and your responsibility.

  • Minimise what you keep. If your question is "how many dentists are in this postcode and how are they rated?", you don't need phone at all. Drop the contact columns at ingest when the analysis doesn't need them β€” it's the cheapest compliance measure available.

  • Ratings and counts are a snapshot. They move constantly, and reviewCount is a text field that can be rendered differently by locale. Publishing "the average rating of X in Y" without the capture date, the sample size and a note on parsing produces a number that looks authoritative and isn't.

  • Listing content belongs to Google and the business. Photos are hosted on Google's CDN and those URLs can rotate. Internal analysis, prospecting and mapping sit on far safer ground than republishing Maps listings as your own directory.

  • Google's Terms of Service prohibit automated access and scraping, and the sanctioned route is the Places API. This Actor does not remove that risk β€” it is a risk you accept by running it. Keep volumes proportionate to the question you're answering.

  • Attribution over mirroring. Every row carries url. Link back to the listing rather than presenting Google's local index as your own.


πŸ†˜ Support & Custom Pipeline Engineering

Need automated multi-city scheduled feeds, webhook enrichment, or enterprise data warehouse syncs?



🏷️ Structured data for search & AI discovery

SoftwareApplication describes the Actor; FAQPage wraps the FAQ above so those answers are eligible for rich results, matching it question-for-question.

{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "Google Maps Scraper",
"alternateName": "Google Maps Scraper β€” Business Listings, Contacts & Coordinates",
"applicationCategory": "BusinessApplication",
"applicationSubCategory": "Web Scraping / Local Business Data & Lead Generation",
"operatingSystem": "Cloud (Apify platform)",
"description": "Runs any Google Maps search β€” one query or many β€” and returns one row per business: name, category, rating, review count, address, phone, website, opening hours, attribute tags, latitude and longitude, Plus Code, listing photo, the Maps URL and the query that produced the row. No Google account, Places API key or billing project is required. Five interface languages, configurable page concurrency, and optional delivery into Airtable, Notion, Slack, Google Sheets, HubSpot or any MCP connector β€” one call per business for CRM-ready records.",
"url": "https://apify.com/techforce.global/google-maps-scraper",
"author": {
"@type": "Organization",
"name": "Techforce Global",
"url": "https://www.techforceglobal.com"
},
"provider": {
"@type": "Organization",
"name": "Apify",
"url": "https://apify.com"
},
"offers": {
"@type": "Offer",
"priceCurrency": "USD",
"category": "Pay per platform usage",
"description": "Usage-based: billed for the compute time and resources a run consumes, not per row. Browser-driven, so cost tracks the number of businesses scraped. Current rates are shown on the Actor's Apify Store page.",
"url": "https://apify.com/techforce.global/google-maps-scraper"
},
"featureList": [
"No Google account, Places API key or billing project required",
"Any free-text Google Maps search, one query or many in a single run",
"Every row stamped with the search query that produced it",
"Name, category, rating and review count from the listing",
"Phone, website and full address for contactable prospects",
"Opening hours captured as a weekly string",
"Latitude, longitude and Plus Code for mapping and territory work",
"Attribute tags and the listing photo",
"Five interface languages: English, Hindi, French, German, Spanish",
"Configurable page concurrency via API, SDK or MCP to balance speed against rate limiting",
"Rows saved as each listing is parsed, so aborted runs keep partial data",
"Per-listing failures isolated - one bad page never sinks the run",
"Optional MCP delivery with one connector call per business for CRM records",
"Connector argument schema printed to the log to make delivery configurable",
"Three Console dataset views including a CRM-ready Contacts view"
],
"isAccessibleForFree": false,
"softwareRequirements": "Apify account and API token",
"keywords": "Google Maps scraper, Google Maps business scraper, Places API alternative, local business lead generation, extract phone numbers from Google Maps, business listings by city, GPS coordinates scraper, local SEO data, sales prospecting, MCP connector"
}
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "Do I need a Google account or a Places API key?",
"acceptedAnswer": {
"@type": "Answer",
"text": "No. The Actor reads publicly visible Maps listings. You need an Apify account and API token; you need no Google credentials, API key or Cloud billing project."
}
},
{
"@type": "Question",
"name": "Is this the official Google Places API?",
"acceptedAnswer": {
"@type": "Answer",
"text": "No. This is an independent Actor and is not affiliated with, endorsed by, or sponsored by Google LLC. The Places API is the sanctioned route and worth using if its billing and caching terms suit you."
}
},
{
"@type": "Question",
"name": "Can I plug it into Claude, Cursor, or a LangChain agent?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Yes β€” it is a native MCP tool: claude mcp add --transport http apify \"https://mcp.apify.com?tools=techforce.global/google-maps-scraper\""
}
},
{
"@type": "Question",
"name": "How do I get more than about 120 results for a city?",
"acceptedAnswer": {
"@type": "Answer",
"text": "You split the city. Google Maps stops feeding any single search at roughly 120 places no matter what maxResults says, so four neighbourhood queries return far more than one city-wide query. Dedupe on url afterwards, because the boundaries overlap."
}
},
{
"@type": "Question",
"name": "Is maxResults per query or per run?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Per query. Three queries at 100 is up to 300 rows in one run."
}
},
{
"@type": "Question",
"name": "Why does my rating sort come out wrong?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Because rating and reviewCount are captured as displayed text, not numbers β€” reviewCount can even arrive as \"(1,234)\" or \"1,234 reviews\". Strip the non-numeric characters and parse both before sorting or averaging."
}
},
{
"@type": "Question",
"name": "Why is tags sometimes null instead of an empty array?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Because the extractor returns null when a listing has no attribute chips at all. Guard for null before iterating, or coalesce to [] at ingest."
}
},
{
"@type": "Question",
"name": "What does the language setting actually change?",
"acceptedAnswer": {
"@type": "Answer",
"text": "More than the interface. Category names, opening-hours labels and tag wording all come back in the chosen language, so category values differ between an en run and a de run. Keep one language per dataset or grouping breaks."
}
},
{
"@type": "Question",
"name": "Can I use a proxy?",
"acceptedAnswer": {
"@type": "Answer",
"text": "There’s no proxy input on this Actor β€” it runs from Apify’s own IPs. If a large run slows down or results thin out, split the search into smaller queries. API, SDK and MCP callers can also lower batchSize to 2–3; the field is hidden in the Console form."
}
},
{
"@type": "Question",
"name": "What is batchSize for?",
"acceptedAnswer": {
"@type": "Answer",
"text": "It’s how many place pages are opened at once. Default 5, range 1–20. Higher is faster but uses more memory and is more likely to trip Google’s rate limiting, so treat it as a throttle rather than a speed setting. It is hidden in the Console form and settable only via API, SDK or MCP."
}
},
{
"@type": "Question",
"name": "Why do so many rows have no phone or website?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Because many businesses simply don't publish them on their listing. Both fields are null in that case β€” filter for contactable rows rather than assuming every result is reachable."
}
},
{
"@type": "Question",
"name": "Is there a place_id?",
"acceptedAnswer": {
"@type": "Answer",
"text": "No. Google's place_id isn't exposed. Use url as your join and de-duplication key, or plusCode where you need a location-stable identifier."
}
},
{
"@type": "Question",
"name": "Why does the same business appear twice?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Because it matched two of your queries, and each row is tagged with its own searchQuery so per-query analysis stays honest. De-duplicate on url or plusCode for a unique list."
}
},
{
"@type": "Question",
"name": "Can I get review text, popular times or menus?",
"acceptedAnswer": {
"@type": "Answer",
"text": "No. This Actor returns the listing's own facts β€” identity, contact, location, hours, rating and review count. Review text, reviewers, popular times, menus and booking links are all out of scope."
}
},
{
"@type": "Question",
"name": "Does it contact the businesses for me?",
"acceptedAnswer": {
"@type": "Answer",
"text": "No, and it can't. MCP delivery pushes rows into connectors you authorised β€” Airtable, Notion, Slack, Sheets, HubSpot. Nothing is ever sent to a business."
}
},
{
"@type": "Question",
"name": "Which delivery mode should I use?",
"acceptedAnswer": {
"@type": "Answer",
"text": "perPlace (the default) when you want one CRM record per business β€” that's exactly what it's for. summary or chunked for a digest into a channel, since perPlace would post one message per business. none for dataset-only runs."
}
},
{
"@type": "Question",
"name": "My connector calls all fail. What now?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Your mcpArguments shape doesn't match what the tool expects. Run once with the connector and tool set, then read the log line \"Connector [tool] expects arguments matching this schema:\" β€” it prints the connector's own JSON Schema. The dataset saves regardless, so a failed delivery costs you nothing but the retry."
}
},
{
"@type": "Question",
"name": "Is the contact data personal data under GDPR?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Sometimes, yes. A switchboard number for a large firm generally isn't, but a sole trader's mobile and home address on their listing very much can be. Read the Compliance & Data Privacy section before you load it into an outreach tool."
}
},
{
"@type": "Question",
"name": "What does it cost, and what do I get on the free plan?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Usage-based: you pay for the platform usage a run consumes, not per row. It's browser-driven β€” one page load per business β€” so the total number of businesses is the cost driver. Free Apify plans return 5 places per query. Pass maxTotalChargeUsd for a hard per-run ceiling."
}
}
]
}