Tutorial
· Updated August 29, 2026
How to Scrape Google Maps Reviews
Fetch public Google Maps reviews for any business as structured JSON. Place IDs, pagination, sort orders, and a working monitoring pipeline.
Written by Alex P.
- Google Maps reviews API
- Google Maps scraper
- local SEO
- review monitoring
- place ID
Google Maps reviews are the highest-signal public feedback source for any business with a physical location. They contain service complaints, staff mentions, wait times, pricing reactions, and the exact language customers use when they describe a place to other people. For competitive research, reputation monitoring, or location-level analytics, they’re more useful than almost anything else that’s publicly available.
The hard part isn’t reading them. It’s turning them into a dataset you can query on a schedule, without maintaining a headless browser that breaks whenever Google adjusts its markup.
This guide covers how to fetch public Google Maps reviews as structured JSON, how place identification actually works (this is where most people get stuck), and how to build a monitoring job on top of it.
Need the endpoint itself? See the Google Maps Reviews API, or the developer reference for full parameter docs.
Why Google Maps scraping is harder than it looks
If you’ve tried this with Puppeteer or Playwright, you already know the problems:
- Reviews are lazy-loaded. The initial HTML contains almost nothing. You have to scroll a virtualized container to trigger each batch, and the scroll target moves between layout revisions.
- Consent and locale interstitials. Depending on the region your IP resolves to, you may hit a consent wall before you see any content at all.
- Aggressive bot detection. Sustained automated scrolling from one address gets rate-limited quickly, and the failure mode is often a silently truncated result rather than an error.
- Unstable class names. Google ships obfuscated, rotating class names. Selectors written against them are guaranteed to break, usually without warning.
The result is that a scraper you write in an afternoon works for a few weeks, then starts returning empty arrays on a Sunday. An API that handles the fetching, rotation, and parsing behind a stable JSON contract removes that whole failure category.
What a review record contains
Each review returned by the endpoint includes:
reviewer— the public display name on the reviewrating— the star rating, 1 to 5text— the review bodyrelativeDate— how Google displays the age of the review (“3 days ago”)likes— how many people marked the review helpful
That’s enough to power rating trend analysis, keyword extraction, complaint clustering, and alerting on new negative reviews.
1. Identify the place
This is the step that trips people up. Google Maps identifies places several different ways, and the endpoint accepts all of them via the placeIdOrUrl parameter:
ChIJ place ID — the canonical form, starting with ChIJ:
ChIJLU7jZClu5kcR4PcOOO6p3I0
Hex place ID — the 0x...:0x... form that appears in some Maps URLs.
A Google Maps place URL — paste the address bar contents directly. The endpoint parses the identifier out of it.
A CID URL or a short link — https://maps.google.com/?cid=…, or the maps.app.goo.gl/… link the Share button gives you.
In practice, the URL form is the one you’ll use most, because you can get it by searching the business in Maps and copying the address bar. If you’re building something that resolves businesses programmatically, store the ChIJ ID — it’s stable, whereas URLs pick up session parameters that change.
2. Fetch the reviews
All five parameters are required on this endpoint. That’s deliberate — it makes the cost of a call explicit rather than letting a default silently pull thousands of reviews:
curl -X POST https://api.fetchlayer.dev/google-maps/reviews \
-H "Authorization: Bearer ss-your-key" \
-H "Content-Type: application/json" \
-d '{
"placeIdOrUrl": "ChIJLU7jZClu5kcR4PcOOO6p3I0",
"pages": 1,
"maxReviews": 20,
"reviewsPerPage": 20,
"sortBy": "mostRelevant"
}'
Response:
{
"reviews": [
{
"reviewer": "Jordan P.",
"rating": 2,
"text": "Waited 40 minutes past our reservation time...",
"relativeDate": "3 days ago",
"likes": 4
}
]
}
Choosing a sort order
sortBy materially changes what you get back, and picking the wrong one is the most common mistake:
| Sort order | What it returns | Use it for |
|---|---|---|
newest | Most recent first | Monitoring, alerting, release/incident tracking |
mostRelevant | Google’s own ranking | A representative sample of overall sentiment |
highestRating | 5-star first | Understanding what customers praise |
lowestRating | 1-star first | Complaint analysis, churn drivers |
For monitoring, always use newest. mostRelevant is Google’s ranking and is not stable over time, so it’s the wrong basis for “what changed since yesterday.”
3. Fetch in JavaScript
async function getMapsReviews(placeIdOrUrl, { pages = 1, sortBy = 'newest' } = {}) {
const res = await fetch('https://api.fetchlayer.dev/google-maps/reviews', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.FETCHLAYER_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
placeIdOrUrl,
pages,
maxReviews: pages * 20,
reviewsPerPage: 20,
sortBy,
}),
});
if (!res.ok) {
throw new Error(`FetchLayer returned ${res.status}: ${await res.text()}`);
}
const { reviews } = await res.json();
return reviews;
}
Note reviewsPerPage is capped at 20 — that’s Google’s own page size, not an arbitrary limit. To get more reviews, increase pages, not reviewsPerPage. Requests are billed per page actually retrieved, so pages: 5 costs five requests.
4. Build a monitoring job
The useful version of this isn’t a one-off pull, it’s a scheduled job that tells you when something changes. The pattern:
import { readFile, writeFile } from 'node:fs/promises';
const PLACE = 'ChIJLU7jZClu5kcR4PcOOO6p3I0';
const SEEN_FILE = './seen-reviews.json';
async function loadSeen() {
try {
return new Set(JSON.parse(await readFile(SEEN_FILE, 'utf8')));
} catch {
return new Set();
}
}
async function checkForNewReviews() {
const seen = await loadSeen();
const reviews = await getMapsReviews(PLACE, { pages: 1, sortBy: 'newest' });
// Fingerprint on reviewer + text, since the endpoint returns a
// relative date ("3 days ago") rather than a stable review ID.
const fresh = reviews.filter((r) => {
const key = `${r.reviewer}::${r.text.slice(0, 80)}`;
if (seen.has(key)) return false;
seen.add(key);
return true;
});
if (fresh.length) {
await notify(fresh.filter((r) => r.rating <= 3));
await writeFile(SEEN_FILE, JSON.stringify([...seen]));
}
return fresh;
}
Two things worth calling out:
Fingerprint, don’t trust dates. relativeDate is a display string, not a timestamp — “3 days ago” becomes “4 days ago” tomorrow. Deduplicate on content, not on date.
Only page 1 for monitoring. If you’re polling with sortBy: newest, everything new is on the first page. Paginating deeper on every run just burns requests re-reading reviews you already have.
Run it on a cron every few hours. For most single-location businesses, checking more often than hourly is wasted spend — review velocity on Maps is low.
If you’d rather not run the scheduler yourself, the same pattern is available as a hosted workflow through the social listening API.
5. Analyze without losing the evidence
The temptation with review data is to feed it straight to an LLM and ask for a summary. That works, but a summary alone isn’t actionable — someone will ask “which reviews say that?” and you won’t be able to answer.
Keep the rows. Structure the analysis so each conclusion points back at the reviews supporting it:
const prompt = `
Below are ${reviews.length} public Google Maps reviews for a business.
Group the complaints into themes. For each theme, return:
- a short label
- how many reviews mention it
- the 2 most representative quotes, verbatim
- the average star rating of reviews in that theme
Do not infer causes that aren't stated in the reviews.
${reviews.map((r, i) => `[${i}] (${r.rating}★) ${r.text}`).join('\n')}
`;
The “do not infer causes” instruction matters. Review analysis is where models most readily invent explanations — a cluster of complaints about wait times becomes “staffing shortages during peak hours,” which may be true but isn’t in the data.
If you’d rather point an AI agent at this directly, the endpoint is also exposed as an MCP tool, so Claude or Cursor can pull the reviews itself.
Legal and practical notes
This accesses only publicly visible reviews — the same content any signed-out browser sees on a business’s Maps listing. It doesn’t touch private data, authenticated endpoints, or anything behind a login.
Two practical constraints worth knowing before you build:
- Reviews are not exhaustive. Google does not surface every review through the public listing, and very old reviews may not be reachable via pagination. Treat the result as a large sample, not a census.
- Reviewer names are personal data. If you’re storing reviews in the EU or UK, the display name is personal data under GDPR even though it’s public. Most teams hash or drop
reviewerunless they specifically need it.
Next steps
- Google Maps Reviews API — endpoint overview and pricing
- Google Maps MCP server — connect this to Claude, Cursor, or Windsurf
- Google Maps Reviews API alternatives — how the options compare
- Documentation — full parameter reference