Google Maps Reputation & Competitor Review Monitor
Pricing
$20.00 / 1,000 successful location checks
Google Maps Reputation & Competitor Review Monitor
Monitor your business and competitors on Google Maps. Detect rating movement, review-count changes, new negative reviews, and recurring reputation trends across scheduled runs.
Pricing
$20.00 / 1,000 successful location checks
Rating
0.0
(0)
Developer
Ed Wheeler
Maintained by CommunityActor stats
0
Bookmarked
1
Total users
0
Monthly active users
7 hours ago
Last modified
Categories
Share
An Apify Actor that watches Google Maps locations over repeated runs and reports what changed: new reviews, rating changes, and review-count movement. Track your own business and competitors side by side so you can see reputation movement relative to the market, not just in isolation.
This is a standard Apify Actor (TypeScript + Crawlee + Playwright). It is not an MCP server and does not run in Standby/web-server mode — it runs to completion and writes its results to the Actor's dataset and key-value store, the same as any other scheduled Actor.
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 silently retries into fabricating a better-looking result — a
"partial"or"failed"status is the honest output, by design.
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), and the request handler's own timeout. 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)