Integration Guide

· Updated August 29, 2026

How to Monitor Google Maps Reviews

Build an automated alert for new Google Maps reviews across one location or hundreds. Polling strategy, deduplication, and multi-location patterns.

Written by Alex P.

  • Google Maps reviews
  • review monitoring
  • reputation management
  • multi-location
  • alerting

A one-star review that sits unanswered for three weeks costs more than the review itself. Google surfaces recent reviews prominently, owner responses visibly affect how complaints read to the next customer, and most review platforms weight recency in their own ranking. The gap between “review posted” and “someone noticed” is the thing worth engineering away.

This guide covers building that alerting loop — for a single location, and for the harder multi-location case where the naive approach gets expensive fast.

This builds on the Google Maps Reviews API. If you haven’t fetched reviews before, start with the scraping guide.

The architecture

Cron (hourly) → Fetch page 1, sortBy: newest → Diff against seen set → Alert on new + low-rated

The whole design rests on one decision: poll page one, sorted by newest, and diff. Everything else follows from that.

Why sortBy matters more than anything else

sortBy: mostRelevant is Google’s own ranking. It’s useful for sampling overall sentiment, and completely wrong for monitoring — the ordering shifts on its own, so a review can appear “new” to your diff simply because Google re-ranked it. You’ll generate false alerts and lose trust in the system within a week.

sortBy: newest gives you a stable, chronological contract: anything genuinely new is at the top of page one. That means:

  • One request per location per check, not five
  • A diff that only produces real positives
  • No need to paginate deeper unless you’re backfilling

For monitoring, this is not a preference. Use newest.

Deduplication: fingerprint the content

The endpoint returns relativeDate — a display string like "3 days ago" — not a timestamp. That’s what Google’s listing shows, and it has a consequence for monitoring: the same review returns a different value tomorrow.

So you can’t key on the date. And there’s no stable public review ID exposed on the listing either. What you have is content:

function fingerprint(review) {
  // Reviewer display name plus a text prefix is stable across polls.
  // Full text would also work, but a prefix keeps the stored set small
  // and survives trailing-whitespace differences between fetches.
  return `${review.reviewer}::${review.text.slice(0, 80)}`;
}

Store the fingerprints, not the reviews, if you only need alerting. A Set of hashes for a few hundred locations is trivially small; the reviews themselves belong in your database only if you’re doing analysis on them later.

A working single-location monitor

import { readFile, writeFile } from 'node:fs/promises';

const API_KEY = process.env.FETCHLAYER_API_KEY;
const PLACE = process.env.PLACE_ID;
const STATE = './review-state.json';
const ALERT_AT_OR_BELOW = 3;

async function fetchNewest(placeIdOrUrl) {
  const res = await fetch('https://api.fetchlayer.dev/google-maps/reviews', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      placeIdOrUrl,
      pages: 1,
      maxReviews: 20,
      reviewsPerPage: 20,
      sortBy: 'newest',
    }),
  });

  if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
  const { reviews } = await res.json();
  return reviews;
}

const fingerprint = (r) => `${r.reviewer}::${r.text.slice(0, 80)}`;

async function run() {
  let seen;
  try {
    seen = new Set(JSON.parse(await readFile(STATE, 'utf8')));
  } catch {
    seen = new Set();
  }

  const reviews = await fetchNewest(PLACE);
  const isFirstRun = seen.size === 0;

  const fresh = reviews.filter((r) => !seen.has(fingerprint(r)));
  reviews.forEach((r) => seen.add(fingerprint(r)));
  await writeFile(STATE, JSON.stringify([...seen]));

  // On a cold start every review looks new. Seed state silently
  // instead of paging someone with 20 historical reviews at 2am.
  if (isFirstRun) {
    console.log(`Seeded ${reviews.length} existing reviews.`);
    return;
  }

  const urgent = fresh.filter((r) => r.rating <= ALERT_AT_OR_BELOW);
  if (urgent.length) await alert(urgent);

  console.log(`${fresh.length} new, ${urgent.length} needing a response.`);
}

The cold-start guard is the part people skip and then regret. Without it, the first run treats all 20 reviews as new and fires 20 alerts.

Scaling to many locations

The naive multi-location version — loop over every place, poll all of them hourly — is where costs get away from you. 400 locations checked hourly is 9,600 requests a day, about 288,000 a month.

Two adjustments cut that dramatically without meaningfully hurting detection time.

Tier by review velocity

Most locations don’t get a review every day. Poll them accordingly:

// Recalculate weekly from observed review counts.
const TIERS = {
  high:   { minPerWeek: 7,  everyHours: 1  },
  medium: { minPerWeek: 1,  everyHours: 6  },
  low:    { minPerWeek: 0,  everyHours: 24 },
};

function tierFor(location) {
  const rate = location.reviewsLastWeek;
  if (rate >= TIERS.high.minPerWeek) return 'high';
  if (rate >= TIERS.medium.minPerWeek) return 'medium';
  return 'low';
}

For a typical multi-location business the distribution is heavily skewed — a handful of flagship locations generate most reviews, and the long tail gets one every few weeks. Tiering usually cuts request volume by 70-80% while keeping detection under an hour where it actually matters.

Stagger, don’t burst

Firing 400 requests at once wastes your rate limit and creates a thundering-herd pattern for no benefit. Spread them:

async function pollBatch(locations, concurrency = 5) {
  const queue = [...locations];
  const workers = Array.from({ length: concurrency }, async () => {
    while (queue.length) {
      const loc = queue.shift();
      try {
        await checkLocation(loc);
      } catch (err) {
        console.error(`${loc.id} failed:`, err.message);
        // Keep going — one bad location shouldn't kill the run.
      }
    }
  });
  await Promise.all(workers);
}

Catching per-location errors matters here. A single place ID that’s been merged or removed shouldn’t abort the run for the other 399.

What to alert on

Not everything deserves a page. A reasonable default split:

ConditionAction
Rating ≤ 2Immediate alert — someone responds today
Rating 3 with textDaily digest — often the most actionable feedback
Rating ≥ 4Weekly rollup, or nothing
Sudden volume spikeAlert regardless of rating — usually signals an incident

That last row catches things rating thresholds miss. Five reviews in an hour at a location that normally gets one a week means something happened, whether they’re angry or delighted.

function isVolumeAnomaly(location, freshCount) {
  const hourlyBaseline = location.reviewsLastWeek / (7 * 24);
  return freshCount > Math.max(3, hourlyBaseline * 10);
}

Routing alerts

Any webhook works. Slack is the common case:

async function alert(reviews) {
  await fetch(process.env.SLACK_WEBHOOK_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      text: `${reviews.length} new review(s) needing a response`,
      blocks: reviews.map((r) => ({
        type: 'section',
        text: {
          type: 'mrkdwn',
          text: `*${'★'.repeat(r.rating)}${'☆'.repeat(5 - r.rating)}* — ${r.reviewer}\n${r.text.slice(0, 300)}`,
        },
      })),
    }),
  });
}

Include the review text in the alert itself. Requiring someone to click through to see whether it’s urgent adds latency to exactly the reviews where latency costs the most.

Practical notes

  • Public reviews only. This reads what any signed-out browser sees. It does not post owner responses — replying still happens through your Google Business Profile.
  • Reviewer names are personal data. Under GDPR, a display name is personal data even when publicly visible. If you’re storing history in the EU or UK, hash the fingerprint rather than keeping raw names, unless you have a reason to keep them.
  • Reviews get deleted. A review disappearing from the listing is normal — Google removes reviews for policy violations, and users delete their own. Your diff should tolerate the set shrinking.

Next steps