Google Maps Lead Scraper - Extract Business Leads
Pricing
from $0.0001 / actor start
Google Maps Lead Scraper - Extract Business Leads
Pull thousands of business leads from Google Maps in minutes. Pick a query and a region, get every business with name, phone, website, address, rating, reviews, and place ID. No browser, no captcha, no API quota. Any country, state, city, or custom area. Optional owner-reply check.
Pricing
from $0.0001 / actor start
Rating
0.0
(0)
Developer
Muhammad Yaser
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
10 days ago
Last modified
Categories
Share
Google Maps Lead Scraper — Bulk Business Leads to CSV
Scrape unlimited Google Maps business leads from any region. Tile a
bounding box (country, state, city, custom area) into a lat/lng grid,
hit Google's internal search?tbm=map endpoint over plain HTTP, and
dedupe by placeId. Pull business name, phone number, website,
address, rating, review count, lat/lng, and place ID for every
business Google Maps knows about — no browser, no captcha hell, no
Google Maps API quota.
Why this scraper is different: normal Google Maps scraping is
capped at ~100 results. The official Google Maps API charges per
request and has tight quotas. This Actor bypasses both by tiling the
region into a grid of small lat/lng cells and pulling 20 results per
cell — thousands of unique leads per run, deduplicated by placeId.
Use cases: B2B sales lead generation · local business research · marketing lists · real estate comps · franchise research · competitor analysis · outreach prospecting.
Try it free — default input is query="plumbers", region
"washington", no card required during the Apify free trial.
What this Actor extracts
For each business, you get:
name— business namecategory— Google Maps category (e.g.Plumber,Restaurant)phone— phone numberwebsite— business website URLaddress— full street addressrating— Google star rating (e.g.4.8)reviews— total review countlat,lng— latitude and longitudeplaceId— Google's stable place ID (use this to fetch more later)mapsUrl— shareable Google Maps URLownerReplied— (optional)1if the owner has replied to any review,0if not,nullif not checked
How it works
- Splits the bounding box into a grid of lat/lng tiles.
- Fires one HTTP request per tile, paginating 20 results at a time per tile while pages come back full.
- Deduplicates by
placeIdso overlapping tiles never produce duplicates. - Pushes each unique lead to the default Dataset and (optionally) writes a CSV to the default Key-Value Store.
- If
checkOwnerReplies=true, fires one extra HTTP request per lead to fetch that place's review data and mark whether the owner has replied to any review.
This breaks the ~100-result cap of normal Google Maps scraping by hitting Google's internal search endpoint, not the public site.
Why use this Actor instead of the Google Maps API or other scrapers?
| This Actor | Google Maps Places API | Browser-based scrapers | |
|---|---|---|---|
| Cost | Free during trial, then $5/mo+ | $0.007–$0.032 per request | Tooling + proxy costs |
| Quota | None | $200/mo free credit, then pay-per-call | Depends on proxy |
| Result cap | None (tiles the region) | 60 results per query | ~100 per session |
| Output | Dataset + CSV | JSON | CSV / DB |
| Phone, website, rating, reviews | ✅ all included | ⚠️ some fields require separate calls | ✅ |
| Owner-reply check | ✅ optional | ❌ | ❌ |
| Set-up | Zero code | API key + billing | Local browser + config |
If you need more than ~100 leads per search, or you don't want to manage a Google Cloud billing account, this Actor is a better fit.
Example queries (just paste in the input)
| Vertical | Try |
|---|---|
| Home services | plumbers, electricians, hvac repair, roofing contractors, locksmith, pest control, landscaping, painters |
| Health & medical | dentists, chiropractors, dermatologists, urgent care, physical therapy, optometrists |
| Real estate | real estate agents, property management, real estate attorneys, title company |
| Auto | auto repair, tire shops, car dealerships, car wash, body shops |
| Food & hospitality | restaurants, cafes, bars, caterers, food trucks, wedding venues |
| Professional services | accountants, lawyers, financial advisors, tax preparation, notary |
| Retail | florists, boutique stores, furniture stores, jewelry stores, pet stores |
| B2B / industrial | manufacturers, warehouses, machine shops, commercial cleaning |
Any service-business query works — Google Maps indexes tens of millions of them. Use a custom region to target a single city, state, country, or your own lat/lng bounding box.
Input
| Field | Type | Default | Notes |
|---|---|---|---|
query | string | plumbers | Search term, e.g. plumbers, hvac repair, restaurants |
region | string | washington | Preset name (washington, gujrat-pk) or a custom spec: custom:latMin,latMax,lngMin,lngMax,latStep,lngStep |
maxPagesPerTile | int | 5 | 20 results per page. 5 = up to 100/tile. Higher = more uniques in dense cities but more requests. |
saveCsv | bool | true | Also write <query>-<region>.csv to the default Key-Value Store |
checkOwnerReplies | bool | false | For each lead, fetch place details and set ownerReplied=1 if the owner has replied to at least one review, 0 if not, null if undetermined. Requires pb-place-template.json (see below). Adds ~1 extra HTTP request per lead. |
For a custom region, get the bbox from
OpenStreetMap's export view and
pick latStep / lngStep based on density (~0.3°/0.45° is ~33 km —
good for whole states; ~0.08°/0.09° is ~9 km — good for dense cities).
Output
Default Dataset — one item per lead, columns:
name, category, phone, website, address, rating, reviews, lat, lng, placeId, mapsUrl, ownerReplied
ownerReplied is 1 (owner has replied to at least one review), 0 (no reply detected), or null (not checked / undetermined).
Default Key-Value Store (when saveCsv is true) — key
<query>-<region>.csv with the same columns.
Local run
Requires Node 20+ and the Apify CLI:
npm installnpx apify run # uses .actor/input_schema.json defaults
To override input, create .actor/input.json (or storage/key_value_stores/default/INPUT.json)
or set APIFY_INPUT_JSON to a JSON string.
$APIFY_INPUT_JSON='{"query":"hvac repair","region":"washington","maxPagesPerTile":3}' npx apify run
Deploy to Apify
npx apify loginnpx apify push
The CLI reads .actor/actor.json, builds the Dockerfile, and pushes
the Actor to your Apify account.
Refresh the internal-search template
The Actor ships with pb-template.json — a captured internal request
parameter Google uses to authenticate search?tbm=map calls. It
expires periodically (days to weeks). When responses stop starting
with )]}', refresh:
- Locally, in the source project (
gmaps-crawlee):$node src/bootstrap-pb.js - Copy the new
pb-template.jsoninto this Actor at the project root. - Re-deploy with
npx apify push.
Enable the owner-reply check (checkOwnerReplies)
The owner-reply check is off by default and requires a second
captured template, pb-place-template.json, that the Actor ships as an
empty placeholder. To enable it:
- Locally, in this Actor (
gmaps-crawlee-apify):
The script opens Chrome, searches Google Maps, clicks the first result, and captures the XHR that loads that place's details (including its reviews and owner replies). It saves the parameterized$node src/bootstrap-pb-place.jspbtopb-place-template.json. - Re-deploy with
npx apify push. - Set
checkOwnerRepliestotruein the Actor's input.
The Actor will then make one extra HTTP request per scraped lead to
check for owner replies. Each lead in the Dataset gets an
ownerReplied field:
1— owner has replied to at least one review0— no reply detectednull— could not determine (template expired, captcha, or parsing miss)
Why an Actor (and not just the CLI)?
- Scheduling — run on a cron, e.g. weekly "fresh leads in my area".
- Webhooks — pipe new datasets to Slack / Sheets / a CRM.
- Billing — charge per run / per 1k results once published.
- Concurrency — Apify's runtime handles memory + headless cleanly.
Notes & limits
- No proxies are bundled. If you hit captchas at scale, add
proxyConfigurationto theHttpCrawleroptions and price accordingly. - Tile size matters: smaller
latStep/lngStep= more uniques in dense cities, more requests. Tuneregionfor the geography. - Dedupe is by
placeId. Re-runs and overlapping tiles never duplicate. - The bundled
pb-template.jsonwas captured against Google Maps' internalsearch?tbm=mapendpoint. It is bound to the same public Terms of Service considerations as any Google Maps scraper.
Project layout
gmaps-crawlee-apify/├── .actor/│ ├── actor.json # Apify Actor manifest│ └── input_schema.json # Input UI schema├── Dockerfile # apify/actor-node:20├── package.json├── pb-template.json # captured internal search template├── pb-place-template.json # captured place-details template (for owner-reply check)├── README.md└── src/├── main.js # scraper, wrapped in Actor.main()└── bootstrap-pb-place.js # one-time capture of pb-place-template.json