Google Maps Scraper: Reviews, Hours & Change Monitoring avatar

Google Maps Scraper: Reviews, Hours & Change Monitoring

Pricing

from $3.00 / 1,000 business profile captureds

Go to Apify Store
Google Maps Scraper: Reviews, Hours & Change Monitoring

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

Farside

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

2 days ago

Last modified

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
}
FieldDefaultMeaning
searchTermsTerms to search for. Combined with location.
locationCity/region to scope the search to.
placeIdsCapture these exact places and skip discovery.
maxPlaces20Upper bound on places captured.
captureReviewstrueEmit review items. Needs a browser.
maxReviewsPerPlace10Reviews per place.
browserEnrichmenttrueUse a browser for weekly hours and popular times.
languageenGoogle interface language.
countryCodeusCountry for the search and the proxy.
proxyUrlOverride the platform residential proxy.
headlessBrowserfalseSee limitations — headless loses reviews.
simulateFrictionnoneTest 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.

TierSourceFields
T0-identityinput onlyplaceId
T1-searchHTTP searchname, address, coordinates, rating, categories
T2-detailHTTP place detailphone, website, full address parts, timezone
T3-reviewsbrowserweekly 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.

EventPriceCharged
result-place$0.003once per place captured
monitor-check$0.0015once 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:

RunCost
100 places, profiles + reviews100 x $0.003 = $0.30
500 places, profiles only (captureReviews: false)500 x $0.003 = $1.50
Daily monitoring of 200 places, 30 days200 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: true so 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.md and MAINTENANCE.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: true will 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. maxReviewsPerPlace is 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 says consent wall: proxy country needs changing). To scrape European places, leave countryCode at us and put the city in location — 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. fieldProvenance and 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.changed relies 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 chromium
npm install # apify-cli as a dev dependency
xvfb-run -a npx apify run # needs a virtual display for the browser tier

Verification tools:

# compare captured places against the live rendered page
xvfb-run -a .venv/bin/python -m tests.spotcheck storage/datasets/default 3
# rebuild tests/fixture.json and measure relative completeness
xvfb-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:

FileWhat it is for
MAINTENANCE.mdwhat breaks first, how to spot it, where to fix it, expected upkeep
docs/CANARY.mdwhat was verified about Google's endpoints, and why the architecture is shaped this way
docs/TIERS.mdthe capability ladder and how degradation is proven
docs/MONITORING.mdchange 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.