Google Maps Scraper — Reviews & Sentiment Analysis
Under maintenancePricing
$19.99/month + usage
Google Maps Scraper — Reviews & Sentiment Analysis
Under maintenanceScrape business listings from Google Maps to collect names, addresses, phone numbers, websites, and more. Perfect for generating local B2B leads or enriching your CRM with location-based contact data.
Pricing
$19.99/month + usage
Rating
5.0
(4)
Developer
Scraper Engine
Maintained by CommunityActor stats
0
Bookmarked
278
Total users
1
Monthly active users
6 days ago
Last modified
Categories
Share
Google Maps Reviews Scraper — Text, Stars, Owner Replies, Photos
Google Maps Reviews Scraper finds places by keyword and area, then collects each place's reviews as structured JSON: review text, star rating, relative and ISO publish dates, the owner's reply and its date, review photos, likes, Local Guide status and the per-review context chips Maps shows (meal type, service, price per person). Places are parent rows, reviews are child rows joined by parentId. Optional AI enrichment adds sentiment, themes and a one-line summary per review using your own LLM provider key. Open it on Apify and press Start.
⚠️ AI enrichment uses your own LLM key and is billed by the provider
aiEnhancement is off by default, and it is worth understanding before you switch it on.
It is not free, and Apify does not bill it. When aiEnhancement is on, the Actor sends review text to the provider implied by aiModel — Anthropic for claude-*, OpenAI for gpt-*, Google for gemini-*, xAI for grok-*, DeepSeek, Perplexity for sonar, Mistral. Those calls are charged to your account with that provider, on their pricing, entirely separately from your Apify usage. Reviews are sent in batches of 20, each review's text truncated to 1500 characters, so your token spend scales with how many reviews you collect, not with how many places.
Review text leaves Apify. Turning this on sends the review body — written by an identifiable person, and sometimes containing details they chose to disclose — to a third-party model provider. Under GDPR that provider becomes a processor in your processing chain: you need it in your record of processing activities, covered by your controller-processor terms, and named in your privacy notice where that notice lists recipients. This is a factual consequence of the feature, not a reason to avoid it, but it is yours to account for. With aiEnhancement off, no review text goes anywhere except your own Apify dataset.
If aiEnhancement is on and no key is found, the run does not fail. The Actor checks aiApiKey first, then the provider's environment variable (ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY / GOOGLE_API_KEY, XAI_API_KEY / GROK_API_KEY, DEEPSEEK_API_KEY, PERPLEXITY_API_KEY / PPLX_API_KEY, MISTRAL_API_KEY). If both are empty it logs a warning naming the variables it looked for, then continues: every review row is still scraped, still written, and still charged as a row_result, with sentiment, sentimentScore, themes and summary present and set to null. The same happens if the client fails to initialise or an individual batch errors — those rows keep null rather than a guessed value. Check the log if the AI columns come back empty; the run itself will look successful.
Four fields are model output, not Google data. sentiment, sentimentScore, themes and summary are inferences produced by the model you selected. Every other field in the output is scraped from what Google renders. The tables below label them explicitly, and no accuracy figure is claimed for them anywhere in this README.
What is Google Maps Reviews Scraper?
Google Maps Reviews Scraper is an Apify Actor that runs two passes: it discovers places from your keywords, locations and Maps URLs, then opens each place in a real browser and reads its reviews feed card by card. Parent rows describe the place; child rows are individual reviews carrying the reviewer, the text, the stars, the dates, the owner's reply, the photos and the review context.
No Google account, login, cookie or API key is required — it reads the same public place pages any visitor sees. An LLM provider key is needed only if you switch AI enrichment on.
It is built for reputation and local-SEO teams monitoring review flow, market researchers comparing what customers actually say across competitors, and developers feeding review text into an analysis or RAG pipeline.
What Google Maps place and review data is publicly available to scrape?
Everything this Actor returns is rendered on a place's public Maps page without signing in: the business record, its rating and review count, and the individual review cards with their authors, text, photos and owner replies.
| Data Category | Public on Maps | Behind a Google sign-in or Business Profile |
|---|---|---|
| Place name, category tags, address, phone, website, coordinates, hours | ✅ Public | — |
| Aggregate rating and total review count | ✅ Public | — |
| Individual review text, star rating and relative date | ✅ Public | — |
| Reviewer display name, profile link, Local Guide badge, review count | ✅ Public | Reviewer's email or account identity |
| Owner replies and their dates | ✅ Public | Posting a reply — Business Profile |
| Review photos, likes, context chips (meal type, service, price per person) | ✅ Public | — |
| Exact posting timestamps to the second | ❌ Maps shows relative dates ("3 months ago") | — |
| Business Profile insights, message threads, review moderation | ❌ | Business Profile owner |
Google Maps Reviews Scraper only returns publicly visible data — what any visitor sees on the place page. Nothing behind a login wall.
Reviewer personal data and the personalData switch
Reviews are personal data. A review card carries a real person's display name, a link to their public contributions profile, their avatar, their Local Guide status and words they wrote themselves.
The Actor ships one switch for this. personalData defaults to true, which returns everything. Set it to false and five fields are blanked to null in every review row before anything is written:
Field blanked when personalData: false | What it is |
|---|---|
name | The reviewer's display name |
reviewerId | Numeric reviewer ID parsed from their profile URL |
reviewerUrl | Link to their public Maps contributions profile |
reviewerPhotoUrl | Their avatar image |
reviewUrl | Deep link to the individual review, which resolves back to its author |
Note that reviewUrl is included. The in-app description mentions the name and profile link; the deep link is blanked too, because it identifies the reviewer just as effectively.
Recommendation: leave personalData off unless reviewer identity is genuinely part of your use case. Sentiment analysis, theme mining, response-rate tracking, rating trends and complaint clustering all work on text, stars, publishAt and responseFromOwnerText alone. Turning identity off at collection time is cleaner than minimising a dataset you have already stored. What personalData: false does not do is redact the review body — a reviewer who signs their own name in the text will still appear there.
What data can I extract with Google Maps Reviews Scraper?
Two row shapes go into the same dataset. type tells them apart: "place" for parents, "review" for children, with isChild as the boolean equivalent and parentId on each review holding its parent's place_id.
Place rows (type: "place") — 23 fields, all scraped
| Field Name | Description |
|---|---|
type | Always "place" on parent rows |
isChild | Always false on parent rows |
parentId | Always null on parent rows |
name | Business name as Google lists it |
tags | Category labels array, e.g. ["Coffee shop", "Espresso bar"] |
notes | Short descriptive note Google attaches to the place, when present |
place_id | Google Place ID — the join key children reference via parentId |
fid | Feature ID (0x…:0x…) Maps uses internally to key the place |
phone | Public phone number |
website | Business website, with Google's redirect wrapper and query string stripped |
street_address | Street line |
city | City |
state | State or region |
zip | Postal code |
country_code | Country code |
full_address | The address parts joined into one line |
lat | Latitude |
long | Longitude |
avg_rating | Aggregate star rating shown on the listing |
total_reviews | Total review count Google reports for the place |
hours | Array of { day, hours, open24Hour, close24Hour } |
reviews_scraped | How many review rows this run actually produced for this place |
success | Always true on written place rows |
Review rows (type: "review") — the review itself, all scraped
| Field Name | Description |
|---|---|
type | Always "review" |
isChild | Always true |
parentId | The parent place's place_id |
reviewId | Google's review ID, used to de-duplicate within a place |
text | Full review body, with "See more" expanded before reading |
stars | Star rating as an integer, 1–5 |
publishAt | Google's relative date string, e.g. "3 months ago" |
publishedAtDate | ISO timestamp derived from that relative string — see the caveat in the Output section |
likesCount | Helpful-vote count on the review, 0 when none |
responseFromOwnerText | The owner's reply, null when there is none |
responseFromOwnerDate | The reply's date as shown on the card |
reviewImageUrls | Array of photo URLs attached to the review |
reviewContext | Object of the review's context chips, e.g. { "Service": "Dine in", "Meal type": "Lunch" } |
visitedIn | The visit period pulled out of reviewContext when that chip exists |
reviewUrl | Deep link to this individual review |
reviewOrigin | Always "Google" |
searchString | The place URL this review was collected from |
scrapedAt | UTC ISO timestamp of collection |
language | The language value the run was configured with |
textTranslated | Always null on this engine — no translation pass runs |
rating | Always null — use stars |
originalLanguage | Always null on this engine |
translatedLanguage | Always null on this engine |
Reviewer fields on review rows — scraped, gated by personalData
| Field Name | Description |
|---|---|
name | Reviewer display name — not the place name on these rows |
reviewerId | Numeric reviewer ID from the profile URL |
reviewerUrl | Public contributions profile link |
reviewerPhotoUrl | Avatar URL, upscaled |
reviewerNumberOfReviews | Total reviews the author has written, parsed from their card line |
isLocalGuide | Whether the card shows the Local Guide badge |
Place context repeated on every review row — scraped
Each review carries its place's details, so a review row stands alone without a join.
| Field Name | Description |
|---|---|
title | Place name |
placeId | Place ID read from the place page |
fid | Feature ID |
cid | Numeric CID derived from the feature ID |
url | Maps search URL for the place |
totalScore | Aggregate rating read from the rendered place header |
reviewsCount | Total review count read from the rendered place header |
address | Address line |
street | Street part |
neighborhood | Neighborhood, when Maps exposes one |
city | City |
postalCode | Postal code |
state | State or region |
countryCode | Country code |
categoryName | Primary category |
categories | All categories as an array |
location | { "lat": …, "lng": … } |
price | Price level string ("$", "$$"…) when shown |
imageUrl | Place cover photo URL |
permanentlyClosed | Boolean flag |
temporarilyClosed | Boolean flag |
AI-derived fields — model output, present only when aiEnhancement is on
These four keys are absent entirely when aiEnhancement is false. When it is true they are always present, and null wherever the model was unreachable, unkeyed, or returned nothing for that review.
| Field Name | Description |
|---|---|
sentiment | Model classification: "positive", "neutral" or "negative" — AI-derived, not published by Google |
sentimentScore | Model score from -1.0 to 1.0 — AI-derived |
themes | Array of 2–5 lowercase topic tags the model assigns, e.g. ["service", "wait time"] — AI-derived |
summary | One-sentence neutral summary the model writes — AI-derived |
Reviews with empty text are never sent to the model; their AI fields stay
null.
Naming conventions to know before you write a mapper
Parent rows use snake_case (place_id, avg_rating, total_reviews, full_address, long); review rows use camelCase (placeId, totalScore, reviewsCount, address, location.lng). The same concept therefore has two names depending on the row type, and name means the place on parents but the reviewer on children — the place name on a review row is title. Branch on type first, then map.
🤖 Add-on: Need additional review data?
Google Play Store Reviews Scraper and Apple App Store Review Scraper cover the app-store side of the same reputation question, which is useful when a local business also ships an app. Goodreads Review Scraper applies the same review-plus-reviewer shape to books. For accommodation reviews, Booking Scraper and Apartments.com Scraper cover the property side.
How does Google Maps Reviews Scraper differ from the official Google Places API?
Google publishes a Places API, and if your use case fits inside it, use it — it is the supported, licensed route with terms you can build a product on. This Actor exists for the case where you need more reviews per place, or the review-card details the API does not carry.
| Feature | Google Places API | Google Maps Reviews Scraper |
|---|---|---|
| Credentials | API key and a billing-enabled Google Cloud project | Apify account only |
| Reviews per place | A capped subset per place — check the current Places API documentation for the exact number | Whatever the reviews feed renders, up to your maxReviewsPerPlace |
| Owner replies, review photos, context chips, likes | Check Google's documentation for what its reviews payload includes | Returned as responseFromOwnerText / responseFromOwnerDate, reviewImageUrls, reviewContext, likesCount |
| Review ordering and filtering | Governed by the API's own parameters | reviewsSort, plus reviewsStartDate and reviewsFilterString applied during and after collection |
| Place discovery | Text and Nearby Search with the API's paging rules | Grid-tiled map search across your locations and keywords, up to maxResults places |
| Cost model | Google bills per request against your Cloud project | Apify pay-per-event, one row_result per row written |
| Usage terms | Places API terms, including caching and display restrictions | Google's general terms — see the legal section below |
Check Google's current published Places API documentation for its exact review count per place, request quotas and pricing before choosing; those terms change and no figures from them are reproduced here.
Use the Places API when you need licensed, contractually supported access and its review subset is enough. Use this Actor when you need review depth, owner replies, photos or context chips, or when you want to sweep a whole city by keyword rather than resolve places one at a time.
How to use Google Maps Reviews Scraper
The Actor runs on Apify. Start it from the Apify Console or call it through the Apify API.
- Open Google Maps Reviews Scraper on Apify and click Try for free
- Fill in What to find (keywords) — the only field the schema marks required, e.g.
["coffee shops"] - Add at least one entry to Cities, regions, or neighborhoods or to Google Maps links. Keywords alone are not enough: a run with neither logs an error and exits without writing rows
- Set How many places to collect (
maxResults) and Max reviews per place (maxReviewsPerPlace). Set the latter to0for places only, with no review rows at all - Optionally set the sort order, a start date, a keyword filter, and turn
personalDataoff if you do not need reviewer identity - Click Start. Each place is written once its reviews finish, so every parent row already carries an accurate
reviews_scrapedcount and its children follow it immediately - Export as JSON, CSV or Excel, or read the dataset through the Apify API
The merged place list is also saved to the run's key-value store as maps.json.
How to scale to bulk review collection
locations, keywords and urls are all lists, and every location is paired with every keyword — three cities and four keywords is twelve searches in one run, all drawing on the same maxResults budget. Places are de-duplicated on place_id across every location, keyword and URL, so overlapping searches never produce the same place twice.
Row volume is the product of both caps: maxResults: 5 with maxReviewsPerPlace: 10 writes up to 5 place rows and up to 50 review rows — 55 charged rows. Start small, confirm the shape, then scale.
What can you do with Google Maps review data?
- 📉 A reputation manager watching a franchise sets
reviewsSort: "newest"andreviewsStartDate: "1 month", then alerts on any row wherestarsis 1 or 2 andresponseFromOwnerTextisnull— an unanswered complaint from the last month. - 🔎 A local SEO consultant pitching a prospect runs
reviewsFilterString: "parking"across every competitor in a suburb and counts how often the complaint recurs pertitle, turning a vague gripe into a countable theme. - 🍽️ A restaurant group comparing formats reads
reviewContextandvisitedInto split reviews by meal type and service style, then comparesstarswithin each segment instead of across one blended average. - 📊 A market analyst sizing a category sweeps several cities with one keyword and uses
avg_rating,total_reviewsandreviews_scrapedon the place rows to rank locations by quality and review volume before drilling into text. - 🤖 An AI engineer building a reputation agent indexes
text,title,starsandpublishAtinto a vector store so the agent can answer "what changed in customer complaints this quarter" against live rows rather than a stale export — and withaiEnhancementon,themesandsentimentbecome ready-made filter metadata.
Every one of these is callable from an agent framework over the Apify API, since the Actor is a standard HTTP-triggered run.
How does Google Maps Reviews Scraper handle rate limits and blocking?
Two passes, two independent strategies.
Place discovery starts on a direct connection with no proxy. If Google answers with 429, 403 or 503, or the body contains block language, the Actor escalates one rung at a time — direct → Apify datacenter → Apify residential — logging each switch, and retries each request up to three times. Setting proxyConfiguration with useApifyProxy: true starts on datacenter instead, or on residential if you include the RESIDENTIAL group.
Review collection ignores proxyConfiguration entirely and always runs through Apify residential proxies pinned to the US, because Maps serves the en-US review UI and skips the EU consent wall on a US exit IP. It opens each place in Chromium — headed under a virtual display when running on Apify, which is less detectable than headless — with an anti-automation init script, a consent cookie set up front, and map-tile and Street View requests aborted to save bandwidth. If the feed renders no review cards at all, the attempt is discarded and the place is retried on a fresh residential IP, up to three rotations. One further guard: if an attempt returns under 60% of the reviews the place plausibly has, it is treated as a throttled session and the IP is rotated rather than accepted. Rows are buffered in memory until an attempt is accepted, so a discarded attempt is never written and never charged.
The Actor detects blocking; it does not solve CAPTCHAs. If every attempt for a place fails, the failure is logged, the place row is still written with reviews_scraped: 0, and the run moves to the next place.
⬇️ Input
Every parameter below is read directly from the input schema. keywords is the only one marked required — but see the pitfalls under the table.
| Parameter | Required | Type | Description | Example Value |
|---|---|---|---|---|
keywords | ✅ Yes | array | What to find — business or place descriptions. Every keyword is paired with every location, so entries multiply your searches. | ["coffee shops", "brunch"] |
locations | No | array | Cities, regions or neighborhoods to search. Each is geocoded to a bounding box, then swept as a grid. | ["Austin, TX", "Downtown Chicago"] |
urls | No | array | Google Maps search URLs to start from, instead of or alongside locations. | ["https://www.google.com/maps/search/coffee/@30.2672,-97.7431,13z"] |
maxResults | No | integer | Total unique places across all locations, keywords and URLs, de-duplicated on Place ID. Minimum 1, maximum 10000. Default 20. | 25 |
maxReviewsPerPlace | No | integer | Reviews collected per place. 0 skips reviews entirely and returns place rows only. Minimum 0, maximum 99999. Default 10. | 50 |
reviewsSort | No | string | Order reviews are collected in: "newest", "mostRelevant", "highestRanking", "lowestRanking". Default "newest". | "lowestRanking" |
reviewsStartDate | No | string | Keep only reviews on or after this date. Absolute ("2025-05-03") or relative ("3 months"). Empty means all-time. Default "". | "6 months" |
reviewsFilterString | No | string | Keep only reviews whose text contains this string, case-insensitive. Empty keeps all. Default "". | "parking" |
language | No | string | Locale for the browser, Maps cookies and Accept-Language. One of en, es, fr, de, it, pt, nl, hi, ar, tr, id, ja, ko, zh-CN. Default "en". | "en" |
personalData | No | boolean | When false, blanks name, reviewerId, reviewerUrl, reviewerPhotoUrl and reviewUrl on every review row. Default true. | false |
aiEnhancement | No | boolean | Adds sentiment, sentimentScore, themes and summary per review using your own provider key. Default false. | false |
aiModel | No | string | Model and, by prefix, provider: claude-haiku-4-5, claude-sonnet-5, claude-opus-4-8, gpt-4o-mini, gpt-4o, gpt-4.1-mini, gemini-2.0-flash-lite, gemini-2.0-flash, gemini-2.5-flash, grok-3-mini, deepseek-chat, sonar, mistral-small-latest. Default "claude-haiku-4-5". | "gpt-4o-mini" |
aiApiKey | No | string | Your LLM provider API key, stored encrypted. Used only when aiEnhancement is on. Falls back to the provider's environment variable. | "sk-…" |
proxyConfiguration | No | object | Apify Proxy settings for the place-discovery pass only. Default is a direct connection that escalates on blocks. | {"useApifyProxy": false} |
Common pitfalls, all verified against the source:
keywordsis required by the schema, but a run also needs at least onelocationsentry or oneurlsentry. Without either, the run logs an error and exits having written nothing.proxyConfigurationaffects place discovery only. Review collection always uses Apify residential US. The Actor reads onlyuseApifyProxyand whetherRESIDENTIALappears inapifyProxyGroups— a country or a custom proxy set in that block is not applied.maxReviewsPerPlaceaccepts up to99999, but each scrape attempt has a scroll budget of 120 seconds plus 4 seconds per requested review, capped at 1500 seconds. Very large values stop on that budget, on Google's feed running out, or on five consecutive rounds without new cards — whichever comes first. Treat the maximum as a ceiling, not a promise.reviewsSortalso accepts the undocumented aliasesrelevance,relevant,highest,ratingandlowest. An unrecognised value logs a warning and falls back to"newest". Applying the sort is best-effort: if the Maps sort menu does not respond, the run logs it and continues in the default order.urlsaccepts plain strings or{"url": "…"}objects — both forms are normalised.- A
reviewsStartDatethat cannot be parsed logs a warning and is ignored rather than failing the run. languagesets the locale and headers only. It does not translate:textTranslated,originalLanguageandtranslatedLanguageare alwaysnull.
Example input
{"locations": ["Austin, TX"],"keywords": ["specialty coffee"],"urls": [],"maxResults": 25,"maxReviewsPerPlace": 50,"reviewsSort": "newest","reviewsStartDate": "6 months","reviewsFilterString": "","language": "en","personalData": false,"aiEnhancement": true,"aiModel": "claude-haiku-4-5","aiApiKey": "<YOUR_PROVIDER_API_KEY>","proxyConfiguration": { "useApifyProxy": false }}
⬆️ Output
Typed, normalized JSON in a single dataset holding both row shapes, with a consistent schema across runs. Export as JSON, CSV or Excel, or read the dataset through the Apify API. Rows are written as each place finishes, so the dataset fills while the run is still going.
Reviews are additionally mirrored to a per-run child dataset named reviews-<runId> — the same rows, uncharged, if you want reviews on their own without filtering.
Charging, stated plainly: every row written to the default dataset is charged as one row_result — place rows and review rows alike. The Actor writes no error rows, no diagnostic rows and no accounting rows, so there is no marker to filter on and no uncharged row type in the default dataset. Failures are logged, not written: a place whose reviews cannot be collected still produces its place row with reviews_scraped: 0 and success: true. To separate the two shapes in code, branch on item["type"] == "review" or on item["isChild"].
Filters do not cost you rows. reviewsFilterString and reviewsStartDate are both applied to buffered records before anything is pushed, so discarded reviews are never written and never charged. The trade-off is that they run against reviews already fetched: the keyword filter consumes the maxReviewsPerPlace budget before filtering, so set that cap higher than the number of matches you want. On reviewsSort: "newest" the date cutoff also stops the scroll as soon as older reviews appear, which saves the rest of the pass; on the other sort orders it only skips non-matching cards while scrolling continues.
One date caveat to plan around: Maps shows relative dates, so publishAt is the string Google renders and publishedAtDate is derived from it by subtraction — an approximation, not Google's exact timestamp. That derivation only understands English "N units ago" phrasing, so on a non-English language setting publishedAtDate comes back null and reviewsStartDate cannot exclude anything, because an unparseable date is kept rather than dropped. For date-filtered runs, keep language: "en".
Example output
A place row:
{"type": "place","isChild": false,"parentId": null,"name": "Radio Coffee & Beer","tags": ["Coffee shop", "Beer garden", "Cafe"],"notes": "Casual counter-serve for coffee, craft beer and tacos","place_id": "ChIJn6wPeUq1RIYRIQ0k1sFGH8s","fid": "0x8644b54a790fac9f:0xcb1f46c1d6240d21","phone": "+1 512-394-7844","website": "https://radiocoffeeandbeer.com","street_address": "4204 Menchaca Rd","city": "Austin","state": "TX","zip": "78704","country_code": "US","full_address": "4204 Menchaca Rd Austin TX 78704 US","lat": 30.2277,"long": -97.7801,"avg_rating": 4.6,"total_reviews": 3184,"hours": [{ "day": "Monday", "hours": "7 AM-12 AM", "open24Hour": "7 AM", "close24Hour": "12 AM" },{ "day": "Tuesday", "hours": "7 AM-12 AM", "open24Hour": "7 AM", "close24Hour": "12 AM" }],"reviews_scraped": 50,"success": true}
A review row from the same run, with personalData: false and aiEnhancement: true:
{"type": "review","isChild": true,"parentId": "ChIJn6wPeUq1RIYRIQ0k1sFGH8s","reviewId": "ChdDSUhNMG9nS0VJQ0FnSUR0eF9YZ2h3RRAB","text": "Came for the cold brew and stayed three hours. Great patio, plenty of shade, and the staff never rushed us. Street parking was the only annoyance — took two laps to find a spot.","stars": 5,"publishAt": "2 months ago","publishedAtDate": "2026-05-26T14:11:08.442Z","likesCount": 4,"responseFromOwnerText": "Thanks for hanging out with us! We hear you on parking — the lot behind the building is open after 6.","responseFromOwnerDate": "2 months ago","reviewImageUrls": ["https://lh3.googleusercontent.com/geougc-cs/AB3l90BqYr8Wk2s1nTf7pQ"],"reviewContext": {"Service": "Dine in","Meal type": "Brunch","Price per person": "$10–20"},"visitedIn": null,"reviewUrl": null,"reviewOrigin": "Google","searchString": "Direct Detail URL: https://www.google.com/maps/search/?api=1&query=Radio%20Coffee%20%26%20Beer&query_place_id=ChIJn6wPeUq1RIYRIQ0k1sFGH8s","scrapedAt": "2026-07-25T09:14:02.481Z","language": "en","textTranslated": null,"rating": null,"originalLanguage": null,"translatedLanguage": null,"name": null,"reviewerId": null,"reviewerUrl": null,"reviewerPhotoUrl": null,"reviewerNumberOfReviews": 87,"isLocalGuide": true,"title": "Radio Coffee & Beer","placeId": "ChIJn6wPeUq1RIYRIQ0k1sFGH8s","fid": "0x8644b54a790fac9f:0xcb1f46c1d6240d21","cid": "14636402841281957153","url": "https://www.google.com/maps/search/?api=1&query=Radio%20Coffee%20%26%20Beer&query_place_id=ChIJn6wPeUq1RIYRIQ0k1sFGH8s","totalScore": 4.6,"reviewsCount": 3184,"address": "4204 Menchaca Rd, Austin, TX 78704","street": "4204 Menchaca Rd","neighborhood": null,"city": "Austin","postalCode": "78704","state": "TX","countryCode": "US","categoryName": "Coffee shop","categories": ["Coffee shop", "Beer garden", "Cafe"],"location": { "lat": 30.2277, "lng": -97.7801 },"price": "$$","imageUrl": "https://lh3.googleusercontent.com/p/AF1QipMx8Kk=w408-h306-k-no","permanentlyClosed": false,"temporarilyClosed": false,"sentiment": "positive","sentimentScore": 0.78,"themes": ["atmosphere", "service", "parking"],"summary": "A positive review praising the patio and unhurried service, with parking as the only complaint."}
How does it work?
Discovery and review collection use two different mechanisms.
Each location is geocoded to a bounding box, which is divided into a grid of map points. The Actor queries Google's map search endpoint at each grid point in parallel and parses the structured place payload it returns — no HTML scraping — and results merge into one de-duplicated list keyed on Place ID. Requests start direct and escalate to Apify datacenter and then residential proxies only if Google pushes back.
Reviews come from the rendered page, because Google now server-renders the reviews feed instead of exposing a separate data call for it. The Actor opens each place in Chromium through an Apify residential US IP, accepts the consent interstitial, opens the reviews tab, applies your sort, then steps down the virtualised feed — reading the cards mounted at each step, expanding "See more" as it goes, and letting Maps lazy-load the next batch. It stops at your cap, at the date cutoff, when the feed stops growing, or on the time budget.
Only publicly visible data is collected, with no Google account or cookie of yours involved. The output schema is fixed in code rather than derived from the page, so field names and types stay stable when Maps changes its markup — a layout change affects whether a field is populated, never what it is called.
Integrations
Google Maps Reviews Scraper is an Apify Actor, so it works with anything that can call the Apify API or read a dataset.
Calling Google Maps Reviews Scraper from Python
from apify_client import ApifyClientclient = ApifyClient("<YOUR_APIFY_TOKEN>")run = client.actor("<YOUR_USERNAME>/google-maps-scraper-reviews-sentiment-analysis").call(run_input={"keywords": ["specialty coffee"],"locations": ["Austin, TX"],"maxResults": 25,"maxReviewsPerPlace": 50,"reviewsSort": "newest","reviewsStartDate": "6 months","personalData": False,})places = {}unanswered = []for item in client.dataset(run["defaultDatasetId"]).iterate_items():if item["type"] == "place":places[item["place_id"]] = item["name"]elif item["stars"] <= 2 and not item.get("responseFromOwnerText"):unanswered.append((places.get(item["parentId"], item["title"]), item["publishAt"], item["text"]))for name, when, text in unanswered:print(f"{name} · {when} · {text[:120]}")
Works in Go, Ruby, Node.js, cURL — any language that can make an HTTP request. Branch on type before touching fields, since parents and children carry different keys.
No-code tools (n8n, Make, LangChain)
In n8n, use the Apify node — or an HTTP Request node pointed at the Actor's run endpoint with your token — and pass the same JSON input; a Switch node on type splits places from reviews into separate branches. In Make, the Apify module supports run-and-wait, so a weekly reputation sweep can feed a Google Sheets, Airtable or Slack step directly. For LangChain, iterate the dataset and build documents from text, with title, stars, publishAt and — when AI enrichment is on — themes as metadata; every value is already a typed primitive or a plain array.
Is it legal to scrape Google Maps reviews?
Collecting publicly visible review data is broadly treated as permissible where no authentication is bypassed, and this Actor reads only what an anonymous visitor sees on a place page — no Google account, no login, no API key.
With personalData: true (the default), the output is personal data. name, reviewerId, reviewerUrl, reviewerPhotoUrl and reviewUrl identify individuals, and the review text itself is content those individuals wrote and may contain further details about them. If you process that for EU or UK data subjects, GDPR and the UK GDPR apply; CCPA/CPRA applies to California residents. In practice that means four things:
- Lawful basis. Decide and document yours before collection — legitimate interests is the usual route for review analytics, and it needs a balancing assessment, not just an assertion.
- Data minimisation. The
personalDatatoggle is the built-in control. Set it tofalseand reviewer identity never enters your dataset, which is a stronger position than collecting it and deleting it later. - Retention and transparency. Set a retention period and honour it, and make sure your privacy notice describes this collection and its source.
- Subject requests. Keep
reviewId— and, if you retained them,reviewerIdandreviewerUrl— so you can locate and delete a specific person's records on request.
One more, and it is easy to miss: aiEnhancement adds a processor to your chain. Enabling it sends review text — personal data written by an identifiable person — to Anthropic, OpenAI, Google, xAI, DeepSeek, Perplexity or Mistral, depending on aiModel. That provider processes it on your behalf under your own account, so it belongs in your record of processing activities, needs to be covered by your controller-processor terms with them, and should appear where your privacy notice lists recipients or international transfers.
Google's own terms of service also apply to how you use what you collect, independently of data-protection law. Consult legal counsel if your use case involves bulk storage of reviewer identity data, republishing review content, or sending review text to a model provider at scale.
❓ Frequently asked questions
What Google Maps review fields does Google Maps Reviews Scraper return?
The five most used are text, stars, publishAt, responseFromOwnerText and title. Review rows also carry reviewContext, reviewImageUrls, likesCount, isLocalGuide and the full place record; place rows carry name, avg_rating, total_reviews, full_address, phone, website, lat/long and hours. See the data fields section above for every key on both shapes.
Does Google Maps Reviews Scraper require a Google account, login or API key?
No. It reads public place pages, and no Google credential of any kind is used. The only credential the Actor can ask for is an LLM provider key, and only if you switch aiEnhancement on — that is a third-party service you sign up for and pay for separately.
How many reviews can I collect per place?
maxReviewsPerPlace accepts up to 99999, but the real ceiling is whichever comes first: your cap, the end of Google's feed for that place, five consecutive scroll rounds without new cards, or the per-attempt time budget of 120 seconds plus 4 seconds per requested review, capped at 1500 seconds. Places with a few hundred reviews finish comfortably; treat five figures as aspirational rather than guaranteed. Across a run, volume is maxResults place rows plus up to maxResults × maxReviewsPerPlace review rows.
What happens if my search returns no places, or a place has no reviews in my date window?
Both fail quietly rather than erroring. A location that cannot be geocoded logs a warning and is skipped, and if no search produces places, the run finishes having written no rows at all — there is no error row to look for. A place whose reviews cannot be collected, or whose reviews all fall outside reviewsStartDate or fail reviewsFilterString, still produces its place row with reviews_scraped: 0 and success: true, with no child rows following it. In code, check reviews_scraped on the parent instead of assuming children exist, and read the log to tell "no matching reviews" apart from "the feed would not render".
Can I scrape multiple places, cities or keywords at once?
Yes. locations, keywords and urls are all lists, and every keyword is run against every location, all sharing one maxResults budget. Places are de-duplicated on Place ID across the whole run, so overlapping searches cannot return a place twice. Reviews are de-duplicated on Google's review ID within each place.
Does the AI sentiment analysis cost extra, and what happens if I forget the key?
Yes, it costs extra, and no, a missing key does not fail the run. AI calls go to your chosen provider on your own account and are billed by them, separately from Apify. If aiEnhancement is on and neither aiApiKey nor the provider's environment variable is set, the Actor logs a warning and carries on: reviews are still scraped, written and charged, with sentiment, sentimentScore, themes and summary all null. Empty AI columns on a successful run almost always mean a missing or rejected key — check the log.
How accurate is the sentiment classification?
No accuracy figure is claimed for it, and you should not assume one. sentiment, sentimentScore, themes and summary are inferences from the model you selected in aiModel, and quality varies by model, by language and by how ambiguous the review is. Everything else in the output is scraped from what Google rendered. Keep text alongside the AI fields so any classification can be checked against the source, and spot-check a sample on your own data before relying on the labels for decisions.
Does Google Maps Reviews Scraper work with Claude, ChatGPT and other AI agent tools?
Yes. It is callable as a standard HTTP endpoint through the Apify API, so LangChain, CrewAI, n8n or a hand-written tool definition can invoke it and receive typed JSON with no parsing step. That is separate from the aiEnhancement option, which is about the Actor calling a model, not a model calling the Actor.
How does Google Maps Reviews Scraper compare to other Google Maps review scrapers?
Checked on the Apify Store on 25 July 2026: pcorral/google-maps-extractor is positioned as an all-in-one lead tool, documenting email and social enrichment, a 0–100 lead score and pre-return filters alongside reviews and AI sentiment, with maxReviewsPerPlace documented as 0–100. strange-advanced-marketing/google-maps-reviews-analyzer documents per-review sentiment scored by a DistilBERT model with a keyword fallback, theme detection, and aggregate summaries including rating distribution and red-flag samples. seibs.co/google-maps-reviews-pro documents a lexicon-based sentiment pass with negation handling, nine-topic clustering, owner-response metrics, a 12-month trend label and a pairwise competitor delta, and states that it wraps an upstream review scraper rather than fetching reviews itself. Observable differences here: this Actor reads the feed directly, exposes the per-review context chips and the owner-reply pair as first-class fields, lets you bring your own model across seven providers instead of a fixed classifier, ships an explicit personalData switch, and claims no sentiment accuracy figure at all.
What happens when Google changes its Maps layout or anti-bot system?
The Actor is maintained, and the output schema is defined in code rather than derived from the page, so your field names and types do not move. Review collection reads rendered markup, which is the part exposed to a redesign: a layout change typically shows up as a field arriving null rather than as a renamed key or a crashed run. Place discovery parses Google's structured map-search payload and is less exposed. The run also logs a per-field coverage check when critical fields fall below threshold, which is the early warning that something upstream moved.
Which Google Maps review fields work best for AI training data and RAG indexing?
For RAG: text is the high-information field, with responseFromOwnerText as a natural paired document and title, categoryName and address as retrieval metadata. For training data and time-series work: stars, likesCount, totalScore, reviewsCount and reviewContext are the most structurally consistent across records. With aiEnhancement on, themes and sentiment make good pre-computed filter metadata — just store them as model output, not as ground truth. All values arrive as typed primitives, arrays or plain nested objects, so no normalisation pass is needed before indexing. Use publishAt rather than publishedAtDate when you need what Google actually displayed.
🔗 Related scrapers
| Scraper Name | What it extracts |
|---|---|
| Google Play Store Reviews Scraper | Android app review text, ratings and versions |
| Apple App Store Review Scraper | iOS review text, ratings and app version per storefront |
| Goodreads Review Scraper | Book reviews, ratings and reviewer details |
| Booking Scraper | Hotel listings, prices and guest review data |
| Apartments.com Scraper | Rental listings with rent, beds, baths and availability |
| Google News Scraper | News articles filtered by publisher and site |
💬 Your feedback
Found a bug, or missing a review field the Maps card shows? Open an issue on the Actor's Issues tab. Reports that include your exact input JSON and the place URL are the fastest to reproduce and fix.