Google Maps Scraper: Reviews, Hours & Change Monitoring
Pricing
from $3.00 / 1,000 business profile captureds
Google Maps Scraper: Reviews, Hours & Change Monitoring
Scrape Google Maps businesses with reviews, weekly opening hours and popular times in one run. Watchlist + diff engine detects phone, website, hours, rating and new-review changes; webhook alerts. Every field carries provenance and confidence. No Google account needed.
Pricing
from $3.00 / 1,000 business profile captureds
Rating
0.0
(0)
Developer
Farside
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
2 days ago
Last modified
Categories
Share
Google Maps Lead Radar
Extract complete Google Maps business profiles — phone, website, weekly opening hours, popular times and reviews — for any city or search term, for lead generation, market research or local competitor tracking. Turn on change monitoring and it watches your target list for you: new reviews, changed phone numbers, moved addresses, rating swings — delivered as events, optionally to your webhook.
What you get
- Complete profiles — name, structured address, phone (E.164), website, categories, rating, review count, weekly opening hours, popular times, coordinates, timezone, photo URLs.
- Reviews with exact timestamps — separate records per review (ID, stars, text, author, publication time), freshest first.
- Change monitoring — baseline + diff engine with 12 event types
(
phone.changed,hours.changed,review.new,status.changed, ...), webhook delivery included. - Per-field provenance — every field records which capture layer produced it and when, so you always know how fresh a phone number is.
Try it in 30 seconds
{"searchTerms": ["pet shop"],"location": "Kuala Lumpur","countryCode": "my","maxPlaces": 12,"maxReviewsPerPlace": 8}
A place costs $0.003 all-inclusive — profile, reviews, weekly hours, popular times, photo URLs, the whole record. Monitoring adds $0.0015 per place per check. 1,000 places = $3. Sample record:
{"type": "place","name": "Dhani Pet Store","addressLine": "38, 40, Jalan Setia Tropika 1/1, Setia Tropika, Johor Bahru","phone": "07-244 4013","rating": 4.7,"reviewCount": 103,"openingHoursWeekly": [{ "weekday": "Monday", "text": "9 am–8 pm", "closed": false }],"tier": "T3-reviews","capturedAt": "2026-09-01T02:50:31+00:00"}
Read on for the full input reference, output schema, tier ladder and honest limitations.
Input
Minimal:
{"searchTerms": ["coffee shop"],"location": "Boulder, CO","maxPlaces": 20}
Specific places, skipping discovery:
{"placeIds": ["ChIJ1S_Bov20RIYRe7MiR8tYawY","ChIJsdcDulXva4cReMhGKCUCClU"],"maxReviewsPerPlace": 20}
Fast, profiles only (no browser, so no weekly hours or popular times):
{"searchTerms": ["dentist"],"location": "Portland, OR","maxPlaces": 50,"captureReviews": false,"browserEnrichment": false}
| Field | Default | Meaning |
|---|---|---|
searchTerms | — | Terms to search for. Combined with location. |
location | — | City/region to scope the search to. |
placeIds | — | Capture these exact places and skip discovery. |
maxPlaces | 20 | Upper bound on places captured. |
captureReviews | true | Emit review items. Needs a browser. |
maxReviewsPerPlace | 10 | Reviews per place. |
browserEnrichment | true | Use a browser for weekly hours and popular times. |
language | en | Google interface language. |
countryCode | us | Country for the search and the proxy. |
proxyUrl | — | Override the platform residential proxy. |
headlessBrowser | false | See limitations — headless loses reviews. |
simulateFriction | none | Test hook for the tier ladder. |
Output
Place item (abridged):
{"type": "place","placeId": "ChIJsdcDulXva4cReMhGKCUCClU","featureId": "0x876bef55ba03d7b1:0x550a02252846c878","name": "O.W.L Coffee Roasters","addressLine": "2355 30th St, Boulder, CO 80301","addressParts": {"neighborhood": "Central Boulder","street": "2355 30th St","city": "Boulder","postalCode": "80301","state": "Colorado","countryCode": "US"},"latitude": 40.024478,"longitude": -105.2541672,"phone": "(720) 790-1077","phoneE164": "+17207901077","website": "https://www.owlcoffeeroasters.com/","categories": ["Coffee shop"],"rating": 4.9,"reviewCount": 47,"openingHoursWeekly": [{ "weekday": "Monday", "text": "8 am–5 pm", "closed": false }],"popularTimes": [[{ "hour": 7, "busyPercent": 15 }]],"photos": [{ "url": "https://lh3.googleusercontent.com/..." }],"timezone": "America/Denver","tier": "T3-reviews","capturedAt": "2026-09-01T03:17:31+00:00","fieldProvenance": {"phone": {"tier": "T2-detail","layer": "http-detail","capturedAt": "2026-09-01T03:17:31+00:00"}}}
Review item:
{"type": "review","placeId": "ChIJsdcDulXva4cReMhGKCUCClU","reviewId": "Ci9DQUlRQUNvZENodHljRjlvT2xSYVVVeE9WbTFvY2pJeE1qazJTMWRVVDJwRU1uYxAB","rating": 5,"text": "Great spot with lovely seasonal offerings...","authorName": "Kat L","authorUrl": "https://www.google.com/maps/contrib/1035539.../reviews","publishedAt": "2026-07-04T18:20:22+00:00","capturedAt": "2026-09-01T03:17:31+00:00"}
Filter the dataset on type to separate places, reviews and the run report.
Capture tiers
Fields are produced by different layers, and the report tells you which.
| Tier | Source | Fields |
|---|---|---|
T0-identity | input only | placeId |
T1-search | HTTP search | name, address, coordinates, rating, categories |
T2-detail | HTTP place detail | phone, website, full address parts, timezone |
T3-reviews | browser | weekly opening hours, popular times, reviews |
T1 and T2 need no browser and are fast. T3 needs a real browser and dominates
run time, which is why browserEnrichment and captureReviews are separate
switches.
Change monitoring
Set watchlistMode: true and each run compares what it captured against the
stored baseline for the same place, emits one event per change, then updates
the baseline. Point it at a placeIds list or a standing search and schedule
it.
Events are dataset items with "type": "changeEvent":
place.baseline, name.changed, phone.changed, website.changed,
address.changed, status.changed, rating.changed, reviewCount.changed,
hours.changed, categories.changed, review.new, photos.new.
{"searchTerms": ["dentist"],"location": "Portland, OR","watchlistMode": true,"webhookUrl": "https://example.com/hooks/maps"}
Set webhookUrl and events are also POSTed there, batched up to 50 per
request. Delivery is best-effort — three attempts, then the run logs it and
carries on; events are in the dataset either way.
Full event reference and the noise-suppression rules: docs/MONITORING.md.
Pricing
Pay per event. You are billed for results, not for runtime.
| Event | Price | Charged |
|---|---|---|
result-place | $0.003 | once per place captured |
monitor-check | $0.0015 | once per monitored place, per check |
A place is all-inclusive at $0.003. That one charge covers the full profile, its reviews, weekly opening hours, popular times and photo URLs. Review items are pushed as separate dataset records for convenience, but they are not billed separately — a place with 100 reviews costs the same $0.003 as one with none. The run report is free.
Monitoring is billed per place, per check — not per change detected. A
check on a place that turned out to be unchanged still costs $0.0015, because
the check is the work being paid for; conversely a place that gained twelve new
reviews since the last run is still a single $0.0015 check, not twelve charges.
Runs with watchlistMode: false are never billed for monitoring.
Worked examples:
| Run | Cost |
|---|---|
| 100 places, profiles + reviews | 100 x $0.003 = $0.30 |
500 places, profiles only (captureReviews: false) | 500 x $0.003 = $1.50 |
| Daily monitoring of 200 places, 30 days | 200 x 30 x ($0.003 + $0.0015) = $27.00 |
Note that a monitoring run also re-captures each place, so it is billed for both the place and the check. Nothing is charged for a place that fails to capture: failures are reported inside the run report rather than pushed as result records, so you are never billed for a result you did not receive.
Long runs and the time budget
Every Apify run has a hard timeout (default 1 hour — visible and
adjustable under Run options in the Console). A run that hits it is killed
by the platform mid-work: partial data, no report, status TIMED-OUT.
This actor avoids that by watching the clock itself. It reads the run's
actual kill time from the platform and stops capturing a safe margin
(~2.5 minutes) before it, so the run always ends SUCCEEDED with:
- every place captured so far, complete and billed normally — never a half-captured record;
- the run report, with
timeBudgetReached: trueso your integration can detect the early stop, and a log line saying how many places were done (time budget reached — stopped after N/M places); - a checkpoint of what was captured. Rerun the actor with the same input and it continues where it stopped, skipping (and not re-billing) the places it already delivered.
As the deadline nears, the actor also stops starting browser captures it can
no longer finish: the last places before the stop may come back without
reviews, weekly hours or popular times (the report's
browserSkippedForTimeBudget flag and per-field provenance show exactly
which). If a run stops because it hit your maximum charge instead, the report
says chargeLimitReached: true and the same rerun-to-continue story applies.
Want large runs to finish in one go? Raise the run timeout in the
Console's run options (or via the API's timeout parameter) — the actor
derives its budget from whatever timeout the run actually has. Otherwise,
just rerun with the same input until the work list is done; the checkpoint
makes reruns cumulative rather than repetitive.
Locally (apify run) there is no platform timeout and the budget is off by
default; export GMAPS_TIME_BUDGET_SECS=<seconds> to impose one — that is
also how the acceptance test exercises this behaviour:
GMAPS_TIME_BUDGET_SECS=75 xvfb-run -a npx apify run --purge.venv/bin/python -m tests.verify_time_budget
Limitations
Read these before relying on the output.
- Anti-bot is an arms race. This reads the same endpoints Google's own web
client uses. Google changes them without notice. When capture degrades, the
parsing layers are isolated (see
docs/CANARY.mdandMAINTENANCE.md) so one field breaks rather than the whole run — but breakage will happen. - Completeness is relative to the listing. Many places have no popular times, no phone, or no website. A field missing from the output usually means Google does not publish it for that place. The run report measures completeness per field so you can tell the difference.
- Reviews need a real browser, and it cannot run headless. Google serves
headless Chromium a reduced view of Maps with no reviews pane. The actor runs
Chromium headed against a virtual display. Setting
headlessBrowser: truewill lose reviews, weekly hours and popular times. - Reviews are a sample, not the full history. Review pagination is the most
fragile part of Maps scraping.
maxReviewsPerPlaceis a ceiling, not a promise; expect the most recent reviews, and fewer than requested on some places. - Search results cap out. A single query returns roughly 120 results at
most, and repeated runs of the same query return slightly different sets.
Results are deduplicated on
placeId. To cover an area properly, split it into several narrower queries. - Datacenter IPs are usually blocked for Maps search. On the platform the actor requests a residential proxy automatically. The browser tier gets its own session-pinned proxy so a page and the requests it triggers share one exit IP.
- Do not set an EU
countryCode. An EU-country proxy lands on Google's consent wall, and the run captures nothing (the log saysconsent wall: proxy country needs changing). To scrape European places, leavecountryCodeatusand put the city inlocation— for example{"searchTerms": ["pet shop"], "location": "Berlin, Germany"}. Profiles, reviews and hours all work that way. - European places are more sensitive to which proxy IP is drawn. Google
serves some exit IPs a reduced view of Maps with no reviews pane, and it does
so far more readily for EU listings than for US or APAC ones. The actor
detects that view, reloads, and then rotates to a fresh proxy session (up to
twice per run) before giving up on a place. Even so, an EU run can come back
with fewer reviews than the same run against a US city, and a run that draws
a bad address early may return profiles with no reviews or weekly hours at
all. Re-running usually clears it.
fieldProvenanceand the run report tell you which places were affected rather than leaving you to guess. - No enrichment beyond Google Maps. No email discovery or verification, no social profiles, no decision-maker identification, no third-party data. What Google Maps shows is what you get.
- Closed-business detection is the least tested field.
status.changedrelies on spotting Google's own closure labels; absence is read as operational. The diff logic is verified, but not against a live permanently-closed listing. - Photos are URLs only. No binaries are downloaded. Google's photo URLs are not guaranteed to be stable indefinitely.
Compliance
This actor reads publicly visible Google Maps pages. It does not log into any Google account, does not post reviews, and does not attempt to bypass login walls. Review text and author names are personal data in many jurisdictions — if you store or process them, that is your responsibility under the applicable privacy law, including having a lawful basis and honouring deletion requests. Scraping of public web data is under active litigation and the legal position differs by jurisdiction; treat this as engineering documentation, not legal advice, and take your own.
Development
python -m venv .venv && .venv/bin/pip install -r requirements.txt.venv/bin/python -m playwright install chromiumnpm install # apify-cli as a dev dependencyxvfb-run -a npx apify run # needs a virtual display for the browser tier
Verification tools:
# compare captured places against the live rendered pagexvfb-run -a .venv/bin/python -m tests.spotcheck storage/datasets/default 3# rebuild tests/fixture.json and measure relative completenessxvfb-run -a .venv/bin/python -m tests.build_fixture
When output looks wrong, start with the health probe — it checks every capture layer and names the file and symbol behind each failure:
xvfb-run -a .venv/bin/python -m tests.canary --browser.venv/bin/python -m tests.privacy_scan
Documentation map:
| File | What it is for |
|---|---|
MAINTENANCE.md | what breaks first, how to spot it, where to fix it, expected upkeep |
docs/CANARY.md | what was verified about Google's endpoints, and why the architecture is shaped this way |
docs/TIERS.md | the capability ladder and how degradation is proven |
docs/MONITORING.md | change events and the rules that keep the feed quiet |
Read docs/CANARY.md before re-deriving anything about Google's endpoints: it
records several approaches that do not work, so you do not spend a day
rediscovering them.