Google Maps Reviews Monitor & Competitor Tracker
Pricing
$20.00 / 1,000 successful location checks
Google Maps Reviews Monitor & Competitor Tracker
Monitor Google Maps reviews, ratings, and competitors across up to 20 locations. Detect new and negative reviews, rating drops, review-count changes, and competitor momentum on recurring runs.
Pricing
$20.00 / 1,000 successful location checks
Rating
0.0
(0)
Developer
AAF Holdings
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
16 days ago
Last modified
Categories
Share
Google Maps Review Monitor & Competitor Tracker
Monitor Google Maps reviews for your business and competitors without manually checking every listing. Track rating changes, review-count growth, new reviews, new negative reviews, and competitor reputation momentum across repeated or scheduled runs.
Use it as a Google review monitor, competitor review tracker, local SEO monitoring tool, or reputation-management feed for agencies and multi-location businesses. Add up to 20 Google Maps locations, run it on a schedule, and get a clean change record for each location plus a summary across your own business and competitors.
Best for
- Local businesses watching rating drops and new negative reviews.
- Marketing and SEO agencies monitoring clients against nearby competitors.
- Multi-location teams tracking review growth without checking Maps manually.
- Automations and AI agents that need structured Google Maps reputation changes through Apify datasets and API access.
What it does
- You give it a list of Google Maps locations (your own business and/or competitors).
- Each run it visits every location with a headless browser, reads the current name, star rating, review count, and a bounded sample of recent reviews.
- It compares that to the snapshot it saved last time (per location, persisted in a
named Apify key-value store) and reports:
- whether this is the first observation for that location (no prior snapshot),
- a rating change and what it changed from,
- the review-count delta since the last run (a simple velocity signal — delta per monitored interval),
- which reviews are new since the last run, and which of those are negative (3 stars or fewer), so a sudden run of bad reviews doesn't get buried.
- It writes one dataset item per location per run, and a run summary (own vs.
competitor average rating, totals for new/negative reviews, rating changes) to the
key-value store under
RUN_SUMMARY.
Run it on an Apify schedule (e.g. daily) and the dataset becomes a time series of your reputation and your competitors', without anyone manually re-exporting review data.
Input
See .actor/input_schema.json for the full schema. Summary:
| Field | Type | Default | Notes |
|---|---|---|---|
locations | array (required) | — | 1–20 entries. Each has url (a google.com/maps.app.goo.gl Google Maps URL — place page, or a maps/place/?q=... search/place-id URL), optional label, and role ("own" or "competitor", default "own"). |
maxReviewsPerLocation | integer | 10 | 1–50. Bounds how many recent reviews are read and stored per location. |
maxConcurrency | integer | 2 | 1–4. Browser pages run concurrently; kept low by default to be gentle on Google Maps. |
navigationTimeoutSecs | integer | 45 | 10–90. Per-page navigation timeout. |
Only https:// URLs on google.com-family hosts under a Maps path are accepted;
anything else is rejected before any browser is launched (this input is untrusted —
it may be a customer's own URL or garbage, and it never reaches a shell or gets
eval'd).
Example input
{"locations": [{ "url": "https://www.google.com/maps/place/?q=place_id:ChIJ...", "label": "My Pizzeria — Main St", "role": "own" },{ "url": "https://www.google.com/maps/place/?q=place_id:ChIJ...", "label": "Competitor Pizzeria", "role": "competitor" }],"maxReviewsPerLocation": 15,"maxConcurrency": 2}
Output
Dataset — one item per location per run (schema in .actor/dataset_schema.json):
{"locationKey": "a1b2c3...","label": "My Pizzeria — Main St","role": "own","sourceUrl": "https://www.google.com/maps/place/?q=place_id:ChIJ...","status": "ok","observedAt": "2026-10-01T12:00:00.000Z","name": "My Pizzeria","rating": 4.6,"reviewCount": 214,"ratingChanged": true,"reviewCountDelta": 3,"newReviewCount": 3,"newNegativeReviewCount": 1,"newReviews": [ { "id": "...", "author": "...", "rating": 2, "relativeDate": "2 days ago", "text": "..." } ]}
status is one of:
"ok"— full data was read."partial"— some data was read (e.g. the name/rating) but another part (e.g. the review list) could not be confirmed this run; see the item'serrorfield for why. This happens for real, documented reasons — see Acquisition reliability below — it is never a guess dressed up as a result."failed"— nothing usable was read for this location this run (e.g. navigation timeout, or the location couldn't be found). Other locations in the same run are unaffected.
Key-value store — RUN_SUMMARY holds a single object per run:
{"status": "ok","requestedLocations": 4,"successfulLocations": 4,"partialLocations": 0,"failedLocations": 0,"ownLocations": 1,"competitorLocations": 3,"newReviews": 5,"newNegativeReviews": 1,"ratingChanges": 1,"reviewCountDeltaTotal": 8,"ownAverageRating": 4.6,"competitorAverageRating": 4.1}
State and scheduling
Prior snapshots are kept in a named key-value store
(google-maps-reputation-monitor-state), separate from the run's default store, so
they survive from one scheduled run to the next. The first run for any given location
always reports firstObservation: true and no changes — there is nothing to diff
against yet, and the Actor will not fabricate a "change" for a baseline. Every run
after that compares against what was actually saved last time.
This means the Actor works naturally with Apify Schedules: point a schedule at it with the same input and it will build up a real change history. Running it twice in a row with no schedule in between is also safe — see Idempotency below.
Acquisition reliability (read this before relying on review counts)
Google serves a GDPR consent interstitial (consent.google.com) to EU-geolocated
traffic before any Maps content is reachable at all. This Actor detects and submits
that consent form automatically (matched by stable hidden-field names, not by
locale-dependent button text) as part of every navigation.
Beyond that, this was directly observed while building this Actor and is the single biggest reliability risk in the product: Google Maps does not consistently serve the same amount of detail for the same URL. The same place URL, hit moments apart with a fresh browser session, sometimes returns the full panel (name, rating, review count, and reviews) and sometimes returns a stripped-down "limited view" (a visible "You're seeing a limited view of Google Maps — sign in for the full experience" banner, no review count, no review list). This is not a parsing bug and not something a different CSS selector fixes — it is Google varying what it serves to a given session.
Rather than guess or silently drop data, this Actor:
- extracts every field independently and never invents a value it couldn't confirm,
- detects the "limited view" banner explicitly and reports it as the reason
(
limited_view_served_by_google) when it's the cause, - marks the location
"partial"(with the specific missing fields listed) rather than"ok"whenever anything had to be skipped, - never fabricates a better-looking result — a
"partial"or"failed"status is the honest output when the data can't be confirmed.
Bounded recovery (no paid services, no extra page loads beyond the cap)
Before reporting a location as partial, the Actor tries three steps, in order, and stops as soon as review count and at least one review are both confirmed:
- Direct: consent handling, then extraction from the loaded page. The review count
is read from the summary button's accessible label (e.g. "1,217 reviews"), then from
the text beside the rating, then from embedded schema.org
aggregateRatingJSON-LD. - Reviews tab: if reviews are not confirmed, open the Reviews panel in the same page (no navigation). Triggers are tried once each in order: the Reviews tab, a tab matched by its text, then the review-count button. "Write a review" is never clicked.
- English-locale reload: only if the limited-view banner is still shown, one
navigation to the same Maps URL with
hl=en&gl=usadded, then the same consent, tab and extraction steps.
Runtime cap per location: at most 2 page loads (the initial load plus the single
reload) and at most 2 Reviews-tab attempts with a 3s click timeout and a 5s wait each. The
request handler timeout is 2 × navigationTimeoutSecs + 120s. A run log line reports
which strategy finished each location (
strategy=direct | reviews_tab | english_locale_reloadIf review data is still unconfirmed after these steps, the location stays "partial" with
limited_view_served_by_google listed first. Only "ok" locations are charged, so
partial results are never billed. A location is "ok" only when review data is confirmed
and no other field is missing. Google can still serve the limited view on every attempt,
and then the honest result is "partial".
No paid third-party Maps API was substituted in to paper over this. The free, direct-navigation path above is what's shipped; its real failure mode is documented here instead of hidden. If you need guaranteed 100%-complete review counts on every single run, that would require Google's paid Places API, which this Actor deliberately does not require you to pay for or configure.
Verify it yourself: npm run smoke:live runs this Actor's actual acquisition code
(not a mock) against a real, public Google Maps place page and prints exactly what was
extracted and which warnings fired.
Idempotency and retries
- Transient page/navigation failures are retried per-location (bounded retries); a retry re-visits that one location, it does not re-run the whole Actor or reprocess locations that already succeeded.
- A location is only ever counted once per run toward pay-per-event charging (see below) — an internal retry on the same location cannot double-charge, because the charge only fires once a location has produced a usable result, guarded by a per-run, per-location set.
- Running the Actor twice in a row with the same input and no intervening changes on Google's side produces the same diff result both times (no new reviews, no rating change) — the state store de-duplicates by content, not by run count.
Resource bounds
Every dimension that could otherwise grow unboundedly from untrusted input is capped:
locations per run (1–20), reviews read per location (1–50), browser concurrency (1–4),
per-page navigation timeout (10–90s), page loads per location (2, see Bounded
recovery), and the request handler's own timeout (2 × navigationTimeoutSecs + 120s). Browser
automation always runs headless with no extra permissions requested.
Pricing (proposed, not applied)
This Actor is designed for Apify's Pay Per Event model but ships unpriced — see docs/PRICING.md for the proposed event, rationale, and double-charge safeguards. No payout configuration, KYC, or terms acceptance has been completed, and the Actor will not be published by this build step.
Support and limitations
- Google Maps' DOM, consent flow, and "limited view" behavior can change at any time;
this Actor has no dependency on an undocumented private API, only on the public page
that any signed-out browser gets, so it degrades (
partial/failed) rather than breaking silently when Google changes something. - It reads what's visible on a standard signed-out Google Maps page. It cannot see reviews Google itself has filtered, and very large locations' full review history is intentionally not scraped in bulk — this is a reputation monitor, not a bulk review exporter.
- It does not post, reply to, or otherwise write anything back to Google Maps.
Local development
npm installnpm run typechecknpm run lintnpm testnpm run smoke:live # hits real Google Maps once, proves acquisition worksnpm run start:dev # runs the Actor locally (reads ./storage, needs an INPUT)