# Google Maps Reviews Scraper (Bad Review Finder) (`simpleapi/google-maps-reviews-scraper`) Actor

Get clean Google Maps review data for insights. The actor collects comments, star ratings, reviewer info, and timestamps. Ideal for customer experience tracking, trend analysis, and data driven decision making.

- **URL**: https://apify.com/simpleapi/google-maps-reviews-scraper.md
- **Developed by:** [SimpleAPI](https://apify.com/simpleapi) (community)
- **Categories:** Automation, Lead generation, Social media
- **Stats:** 10 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.99 / 1,000 results

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

### Google Maps Reviews Scraper (Bad Review Finder)

Google Maps Reviews Scraper (Bad Review Finder) reads a place's public Google Maps reviews and returns only the ones worth a reply: the star ratings you choose, with real written text, in the languages your team answers in, optionally limited to reviews with no owner response yet. Every row also carries the place's whole `ratingDistribution` — a 1-to-5-star count pulled from Google's own histogram — plus `negativeReviewShare`, `text`, `stars`, `reviewerNumberOfReviews`, and `responseFromOwnerText`. Built for support teams, reputation-management agencies, and franchise auditors who need a complaint queue, not a full review dump.

### What is Google Maps Reviews Scraper (Bad Review Finder)?

It is an Apify Actor that opens a Google Maps place, drives its live reviews feed, and pushes one dataset row per review that survives a complaint filter you configure — star rating, written-text requirement, minimum length, review language, and reply status. No Google account, login, or API key is required: the Actor reads the same public reviews tab a visitor sees. The filter runs inside the scroll loop itself, so `maxReviews` counts matching reviews and the feed keeps reading (up to `maxReviewsScanned`) until that many complaints exist or the read budget runs out.

- **Discovery by URL or Place ID** — feed it one or more Google Maps place URLs (`startUrls`) or raw Place IDs (`placeIds`); both are audited in the same run
- **A pre-charge complaint filter** — `starRatings`, `requireWrittenText`, `minTextLength`, `reviewLanguages`, and `onlyWithoutOwnerReply` all run before a review is buffered, so you are never charged for a rejected row
- **Place-wide rating context** — `ratingDistribution` (the place's full 1★–5★ histogram), `negativeReviewCount`, and `negativeReviewShare` ride on every row, computed once per place
- **Full reviewer identity** — `name`, `reviewerUrl`, `reviewerPhotoUrl`, `reviewerNumberOfReviews`, `isLocalGuide` — or blank all five with `personalData: false`
- **Owner-reply and sub-rating detail** — `responseFromOwnerText`/`responseFromOwnerDate`, `hasOwnerReply`, per-aspect `reviewDetailedRating` (Food/Service/Atmosphere-style scores), and `reviewContext` chips (Service, Meal type, Price per person, etc.)
- **Standard Apify dataset exports** — JSON, CSV, Excel/XLSX, and the Dataset API, with no extra configuration

### What data can I extract with Google Maps Reviews Scraper (Bad Review Finder)?

Every row combines the individual review, the reviewer, the place it belongs to, and the run's own filter accounting — all under the exact JSON key names the Actor writes.

| Field | Example Value | Use Case |
| --- | --- | --- |
| `searchString` | `"https://www.google.com/maps/place/Boucherie+Union+Square/..."` | Trace a row back to the input URL or Place ID that produced it |
| `reviewerId` | `"104827365991822334455"` | Stable numeric id for joining a reviewer across runs |
| `reviewerUrl` | `"https://www.google.com/maps/contrib/104827365991822334455"` | Link to the reviewer's Maps contributor profile |
| `name` | `"Priya N."` | Display name for a review-management dashboard |
| `reviewerNumberOfReviews` | `47` | Weight a review by how experienced the reviewer is |
| `isLocalGuide` | `true` | Flag reviews from Google's Local Guide program |
| `reviewerPhotoUrl` | `"https://lh3.googleusercontent.com/a-/ALV-...=s1920"` | Full-resolution avatar for a review-wall UI |
| `text` | `"Waited 40 minutes for a table..."` | The complaint text itself — what a manager actually reads |
| `textLength` | `127` | Filter out one-word ratings before triage |
| `publishAt` | `"3 months ago"` | The exact string Google renders on the card |
| `publishedAtDate` | `"2026-05-01T00:00:00.000Z"` | Sortable/derived date for trend charts |
| `likesCount` | `4` | Rank complaints by how many people agreed |
| `reviewId` | `"ChdDSUhNMG9nS0VJQ0FnSURwd2R6cmZ3"` | Google's own review identifier, for dedup |
| `reviewUrl` | `"https://www.google.com/maps/reviews/data=..."` | Deep link straight to the review on Maps |
| `stars` | `2` | The rating the complaint filter matched on |
| `responseFromOwnerDate` | `null` | Age of the business's reply, if one exists |
| `responseFromOwnerText` | `null` | The business's own reply text, if any |
| `hasOwnerReply` | `false` | Boolean shortcut instead of checking `responseFromOwnerText` for null |
| `reviewImageUrls` | `["https://lh3.googleusercontent.com/p/AF1..."]` | Photos the reviewer attached, for a visual audit |
| `reviewContext` | `{"Service": "Dine in", "Meal type": "Dinner"}` | Google's own context chips for the visit |
| `reviewDetailedRating` | `{"Food": 3, "Service": 1, "Atmosphere": 3}` | Per-aspect sub-scores, when the reviewer left them |
| `originalLanguage` | `"en"` | The review's own published language tag |
| `placeId` | `"ChIJc1X1HqFZwokRvupG9K0Xd_I"` | Google's canonical place identifier |
| `cid` | `"3442553861032018645"` | Decimal customer id, derived from the place's feature id |
| `fid` | `"0x89c259a1ec5f5573:0x2fc6687f46f682d5"` | Google's internal feature id, from the Maps URL |
| `location` | `{"lat": 40.7372552, "lng": -73.9882246}` | Coordinates for mapping or radius work |
| `address` | `"225 Park Ave S, New York, NY 10003"` | Full mailing address of the place |
| `neighborhood` | `"Union Square"` | Sub-city area for local segmentation |
| `street` | `"225 Park Ave S"` | Street line, split out from the full address |
| `city` | `"New York"` | City-level rollups across a portfolio |
| `postalCode` | `"10003"` | ZIP/postal-level segmentation |
| `state` | `"NY"` | State/region rollups |
| `countryCode` | `"US"` | Two-letter country code |
| `categoryName` | `"French restaurant"` | Google's primary category label |
| `categories` | `["French restaurant", "Bistro"]` | Full category list, for niche filtering |
| `title` | `"Boucherie Union Square"` | The place's display name |
| `totalScore` | `4.7` | The place's overall published Google rating |
| `reviewsCount` | `5218` | The place's total published review count |
| `url` | `"https://www.google.com/maps/search/?api=1&query=..."` | A stable Maps search link back to the place |
| `price` | `"$$"` | Google's price-range indicator |
| `imageUrl` | `"https://lh3.googleusercontent.com/p/AF1...=w408-h306-k-no"` | A representative photo of the place |
| `language` | `"en"` | The Maps interface language actually served for this row |
| `scrapedAt` | `"2026-08-16T14:02:31.118Z"` | Freshness stamp for dedup/refresh logic |
| `ratingDistribution` | `{"oneStar": 103, "twoStar": 80, "threeStar": 189, "fourStar": 484, "fiveStar": 4362}` | The place's whole 1-to-5-star mix, the honest denominator |
| `negativeReviewCount` | `183` | `oneStar` + `twoStar`, computed once per place |
| `negativeReviewShare` | `0.0351` | Negative reviews as a share of the place's total, 0–1 |
| `reviewFilterSummary` | `"stars ∈ {1,2} · written text required · text ≥ 40 chars · no owner reply yet"` | Human-readable record of which filter produced this run |
| `scannedReviewsCount` | `214` | How many cards were read for this place, matching or not |
| `matchedReviewsCount` | `15` | How many of those cards survived the filter |
| `droppedByStarsCount` | `172` | Cards rejected for the wrong (or unreadable) star rating |
| `droppedRatingOnlyCount` | `19` | Star-only cards rejected for carrying no written text |
| `droppedByLengthCount` | `6` | Cards rejected for text shorter than `minTextLength` |
| `droppedByLanguageCount` | `0` | Cards rejected for a language outside `reviewLanguages` |
| `droppedByOwnerReplyCount` | `2` | Cards rejected because the business had already replied |
| `unlabelledLanguageKeptCount` | `0` | Reviews Google left untagged, kept rather than guessed |

#### 📊 Rating context and filter accounting

Every row carries two layers of numbers that are not about the individual review at all. The first is `ratingDistribution` — the place's full 1★–5★ histogram, read from a deterministic index of the same payload Google's own reviews pane fires, cross-checked against the place's published `totalScore` before it is trusted. `negativeReviewCount` and `negativeReviewShare` are derived from it, so you always know what fraction of the place's whole review history the complaints in this run represent — "183 negative out of 5,218" — without paying to scrape all 5,218. The second layer is the run's own accounting: `scannedReviewsCount`, `matchedReviewsCount`, and five `droppedBy*Count` fields explain exactly why a short result is short, instead of leaving you to guess whether the filter was too strict or the place simply has few complaints.

#### 🗺️ Location and business identity fields

`address`, `neighborhood`, `street`, `city`, `state`, `postalCode`, `countryCode`, and `location` (lat/lng) let you group complaint rows by market without a second lookup — useful for rolling a franchise's complaint queue up by region. `placeId`, `fid`, and `cid` are Google's own identifiers for joining rows back to a place across runs, and `categoryName`/`categories`/`price` let you segment a multi-vertical portfolio (restaurants vs. retail vs. services) before triaging complaints.

### How does Google Maps Reviews Scraper (Bad Review Finder) differ from the official Google Places API?

Google's own Places API (New) caps how much review data you can pull per place at all, filter-free. Per Google's Place reference: the `reviews[]` field is "sorted by relevance," and "a maximum of 5 reviews can be returned" per place — there is no parameter to request more, to sort by rating, or to page past the fifth review (Google, Places API (New) reference, checked 2026-08-16).

| Feature | Google Places API (New) — Place Details `reviews` field | Google Maps Reviews Scraper (Bad Review Finder) |
| --- | --- | --- |
| Reviews returned per place | Maximum 5, sorted by relevance only | Up to `maxReviews` matching reviews (max 1,000), found by reading up to `maxReviewsScanned` cards (max 3,000) |
| Filter by star rating | Not available | `starRatings` keeps only the ratings you pick |
| Filter by written text / length | Not available | `requireWrittenText` + `minTextLength` |
| Filter to unanswered reviews only | Not available | `onlyWithoutOwnerReply` |
| Place-wide 1-to-5-star breakdown | Only an average `rating` and a `userRatingCount` total — no per-star counts | `ratingDistribution`, `negativeReviewCount`, `negativeReviewShare` on every row |
| Sort order | Fixed to relevance | `reviewsSort`: newest, most relevant, highest, or lowest first |
| Setup | Google Cloud project, API key, billing enabled | Apify account only — no Google credentials |
| Cost basis | Billed per request, at the SKU tier your requested fields fall into | Billed per matching review row (`row_result`) |

#### Why can't I get more than 5 reviews from Google's own API?

Google's Place resource documentation fixes the `reviews[]` field at five entries per place, sorted by relevance, with no pagination or rating filter (Google, Places API (New) reference, checked 2026-08-16). For a single location that is a minor inconvenience; for auditing a 50-location franchise it means at most 250 reviews total across the whole portfolio, hand-picked by Google's relevance model rather than by star rating — with no way to isolate the one- and two-star complaints inside them. This Actor sidesteps the field entirely: it drives the actual public reviews feed the way a person scrolling the page would, reading review cards straight out of the DOM, so the review count is bounded only by `maxReviewsScanned` rather than a fixed cap of five.

The official API is the right tool when you already administer the place and only need its aggregate `rating`/`userRatingCount` inside your own app. For pulling review-level detail filtered by star rating, text, language, or reply status — across one place or a whole portfolio — that filtering does not exist through the official API today, which is what this Actor is built to cover.

### How to use data extracted from Google Maps reviews?

#### 🎧 Customer support and reputation-management teams

Point `startUrls` at your own location (or a client's), set `starRatings` to `["1","2"]` with `onlyWithoutOwnerReply: true`, and the run returns exactly the backlog a support lead needs to work through this morning — `text`, `stars`, `publishAt`, and `reviewUrl` to reply directly. `reviewFilterSummary` on every row documents which filter produced the queue, so a weekly export is self-explanatory to anyone who opens it.

#### 🏢 Agencies auditing client locations

Run the same `starRatings`/`onlyWithoutOwnerReply` combination across every client location in one `startUrls` list on a schedule, then compare `negativeReviewShare` and `matchedReviewsCount` per place in a single dataset export — a recurring benchmarking workflow with no per-location manual pull, and a defensible "here's what changed since last month" report.

#### 🔬 Market research and franchise-performance analysis

Feed in every location in a chain or a competitor set and group rows by `city`/`state` alongside `ratingDistribution` to see whether complaint density clusters by region rather than by individual store, using `categoryName` to keep verticals separate in a mixed portfolio.

#### 🤖 AI agents and automated pipelines

Because the output is flat, typed JSON with `text`, `stars`, `reviewDetailedRating`, and `reviewFilterSummary` already resolved, it drops directly into an agent tool call for "summarise this week's unanswered one-star reviews for location X" or a RAG index over a support team's complaint history, with no normalization step first.

### 🔼 Input sample

No parameter is required — the schema's own `startUrls` prefill and defaults run a 1★/2★ complaint sweep on their own.

| Parameter | Required | Type | Description | Example Value |
| --- | --- | --- | --- | --- |
| `startUrls` | No | Array of strings | One or more Google Maps place URLs. Plain strings and `{"url": "..."}` objects both work. | `["https://www.google.com/maps/place/Boucherie+Union+Square/..."]` |
| `placeIds` | No | Array of strings | Raw Google Place IDs (`ChIJ...`); each is turned into a Maps URL and audited alongside `startUrls`. | `["ChIJc1X1HqFZwokRvupG9K0Xd_I"]` |
| `starRatings` | No | Array of strings, select (`1`–`5`) | Which star ratings count as a complaint. Default `["1", "2"]`. Selecting all five turns the star filter off. A card whose star widget could not be read is rejected, never assumed negative. | `["1", "2"]` |
| `requireWrittenText` | No | Boolean | Drop star-only ratings with no written text. Default `true`. | `true` |
| `minTextLength` | No | Integer (0–2000) | Minimum character length for the review text. Default `40`. Set `0` to keep any text. | `40` |
| `reviewLanguages` | No | Array of strings | Keep only reviews tagged in these language codes. Default `[]` (all languages). Untagged reviews are kept, never guessed. | `["en", "es"]` |
| `onlyWithoutOwnerReply` | No | Boolean | Keep only reviews with no business reply yet. Default `false`. | `true` |
| `maxReviews` | No | Integer (1–1000) | How many reviews that PASS the filter to keep per place. Default `10`. | `15` |
| `maxReviewsScanned` | No | Integer (1–3000) | Hard ceiling on review cards READ per place, matching or not. Default `300`. You are not charged for rejected cards. | `500` |
| `reviewsSort` | No | String, enum: `newest`, `mostRelevant`, `highestRanking`, `lowestRanking` | Feed order Google is read in. Default `"newest"`. `lowestRanking` puts complaints first and is the cheapest way to fill a 1-2 star queue. | `"lowestRanking"` |
| `reviewsStartDateAbsolute` | No | String (date) | Exact cutoff date (`YYYY-MM-DD`). Accurate to Google's published relative-age bucket, not the exact day. Default `""`. | `"2026-01-01"` |
| `reviewsStartDateRelative` | No | String, enum: `""`, `"7 days"`, `"1 month"`, `"3 months"`, `"6 months"`, `"1 year"`, `"2 years"` | Rolling-window cutoff, used only when the absolute date is empty. Default `""`. | `"3 months"` |
| `language` | No | String, enum: `en`, `es`, `fr`, `de`, `it`, `pt`, `nl`, `bn`, `hi`, `ar`, `tr`, `id`, `ja`, `ko`, `zh-CN` | Interface language of the Maps pages fetched, applied as `hl=<code>&gl=us`. Changes `publishAt`, `categories`, `price`. Does NOT filter reviews — use `reviewLanguages`. Default `"en"`. | `"en"` |
| `personalData` | No | Boolean | When `false`, `name`, `reviewerId`, `reviewerUrl`, `reviewerPhotoUrl`, and `reviewUrl` are emitted as `null`. Default `true`. | `true` |
| `proxy` | No | Object (JSON editor) | Optional custom proxy (`use_proxy`, `host`, `port`, `username`, `password`). Left as-is, the Actor uses Apify Residential (US) with automatic IP rotation. | `{"use_proxy": false}` |

```json
{
  "startUrls": ["https://www.google.com/maps/place/Boucherie+Union+Square/@40.7372552,-73.9882246,17z/data=!4m6!3m5!1s0x89c259a1ec5f5573:0x2fc6687f46f682d5!8m2!3d40.7372552!4d-73.9882246!16s%2Fg%2F11hbv5rh0_"],
  "starRatings": ["1", "2"],
  "requireWrittenText": true,
  "minTextLength": 40,
  "reviewLanguages": ["en"],
  "onlyWithoutOwnerReply": true,
  "maxReviews": 15,
  "maxReviewsScanned": 500,
  "reviewsSort": "lowestRanking",
  "language": "en",
  "personalData": true
}
```

**Common pitfall:** `maxReviews` caps MATCHING reviews, not cards read — `maxReviewsScanned` is the real budget on run time and how deep the hunt goes. A run that returns fewer rows than `maxReviews` is not necessarily broken: check `droppedByStarsCount`, `droppedRatingOnlyCount`, `droppedByLengthCount`, `droppedByLanguageCount`, and `droppedByOwnerReplyCount` on any row (or the run log) to see which filter absorbed the rest, then either widen `starRatings`, lower `minTextLength`, or raise `maxReviewsScanned`. Also note `reviewsStartDateAbsolute` always wins over `reviewsStartDateRelative` when both are set — the relative window is only used when the absolute date is left empty.

### 🔽 Output sample

Each matching review is pushed to the dataset the moment it survives the filter — typed, normalized JSON with a consistent schema across places. Export directly from the Apify Console as JSON, CSV, or Excel, or read it back through the Dataset API.

```json
{
  "searchString": "https://www.google.com/maps/place/Boucherie+Union+Square/@40.7372552,-73.9882246,17z/data=!4m6!3m5!1s0x89c259a1ec5f5573:0x2fc6687f46f682d5!8m2!3d40.7372552!4d-73.9882246!16s%2Fg%2F11hbv5rh0_",
  "reviewerId": "104827365991822334455",
  "reviewerUrl": "https://www.google.com/maps/contrib/104827365991822334455",
  "name": "Priya N.",
  "reviewerNumberOfReviews": 47,
  "isLocalGuide": true,
  "reviewerPhotoUrl": "https://lh3.googleusercontent.com/a-/ALV-abc123=s1920",
  "text": "Waited 40 minutes for a table we'd reserved online, then the order came out wrong twice. Nobody from the front desk apologized.",
  "publishAt": "3 months ago",
  "publishedAtDate": "2026-05-01T00:00:00.000Z",
  "likesCount": 4,
  "reviewId": "ChdDSUhNMG9nS0VJQ0FnSURwd2R6cmZ3",
  "reviewUrl": "https://www.google.com/maps/reviews/data=!4m6!14m5!1m4!2m3!1sChdDSUhNMG9nS0VJQ0FnSURwd2R6cmZ3!2m1!1s0x0:0x2fc6687f46f682d5?hl=en",
  "stars": 2,
  "responseFromOwnerDate": null,
  "responseFromOwnerText": null,
  "reviewImageUrls": ["https://lh3.googleusercontent.com/p/AF1abcXYZ"],
  "reviewContext": {"Service": "Dine in", "Meal type": "Dinner"},
  "reviewDetailedRating": {"Food": 3, "Service": 1, "Atmosphere": 3},
  "originalLanguage": "en",
  "placeId": "ChIJc1X1HqFZwokRvupG9K0Xd_I",
  "location": {"lat": 40.7372552, "lng": -73.9882246},
  "address": "225 Park Ave S, New York, NY 10003",
  "neighborhood": "Union Square",
  "street": "225 Park Ave S",
  "city": "New York",
  "postalCode": "10003",
  "state": "NY",
  "countryCode": "US",
  "categoryName": "French restaurant",
  "categories": ["French restaurant", "Bistro"],
  "title": "Boucherie Union Square",
  "totalScore": 4.7,
  "reviewsCount": 5218,
  "url": "https://www.google.com/maps/search/?api=1&query=Boucherie+Union+Square&query_place_id=ChIJc1X1HqFZwokRvupG9K0Xd_I",
  "price": "$$",
  "cid": "3442553861032018645",
  "fid": "0x89c259a1ec5f5573:0x2fc6687f46f682d5",
  "imageUrl": "https://lh3.googleusercontent.com/p/AF1placeXYZ=w408-h306-k-no",
  "scrapedAt": "2026-08-16T14:02:31.118Z",
  "language": "en",
  "textLength": 127,
  "hasOwnerReply": false,
  "ratingDistribution": {"oneStar": 103, "twoStar": 80, "threeStar": 189, "fourStar": 484, "fiveStar": 4362},
  "negativeReviewCount": 183,
  "negativeReviewShare": 0.0351,
  "reviewFilterSummary": "stars ∈ {1,2} · written text required · text ≥ 40 chars · no owner reply yet",
  "scannedReviewsCount": 214,
  "matchedReviewsCount": 15,
  "unlabelledLanguageKeptCount": 0,
  "droppedByStarsCount": 172,
  "droppedRatingOnlyCount": 19,
  "droppedByLengthCount": 6,
  "droppedByLanguageCount": 0,
  "droppedByOwnerReplyCount": 2
}
```

### How do you filter and target specific reviews?

This is the core of the Actor: the complaint filter runs on every review card **before** it is buffered or charged, so `maxReviews` counts matches, not cards read. Four axes combine to define a "complaint": **star rating** (`starRatings` — leave all five selected to disable it, or narrow to `["1","2"]`/`["1","2","3"]`), **content quality** (`requireWrittenText` plus `minTextLength`, so a bare tap on one star doesn't count), **language** (`reviewLanguages`, matched against each review's own published language tag — never guessed, and an untagged review is kept rather than dropped), and **reply status** (`onlyWithoutOwnerReply`, to isolate the actual open work queue). `reviewsSort` decides which cards get read first — `lowestRanking` reads negative reviews before positive ones and is the cheapest way to fill a 1–2★ queue, while `newest` answers "what came in this week." Volume is controlled by two separate budgets: `maxReviews` (how many matches you want, up to 1,000) and `maxReviewsScanned` (how many cards may be read while hunting for them, up to 3,000) — the second is automatically raised if it's set below the first.

```json
{ "starRatings": ["1", "2"], "onlyWithoutOwnerReply": true, "reviewsSort": "lowestRanking", "maxReviews": 20, "maxReviewsScanned": 400 }
```

```json
{ "starRatings": ["1", "2", "3"], "reviewLanguages": ["en", "es"], "requireWrittenText": true, "minTextLength": 60 }
```

```json
{ "starRatings": ["1", "2"], "reviewsStartDateRelative": "3 months", "maxReviews": 50, "maxReviewsScanned": 1500 }
```

### ▶️ Want to try other Google Maps and directory scrapers?

| Scraper | What it extracts |
| --- | --- |
| [Google Play Scraper — Country Pricing, IAP Tiers and Availability](https://apify.com) | Per-country Google Play Store pricing, in-app purchase tiers, and storefront availability |
| [2GIS Places Scraper — by Direct 2GIS Links](https://apify.com) | 2GIS place records — address, coordinates, hours, ratings, contacts and reviews — from links or IDs |
| [Facebook Reviews Scraper — Headlines, Tags & Reviewer Detail](https://apify.com) | Public Facebook Page reviews with Facebook's own headline, tags, and resolved reviewer identity |
| [Airbnb Review Scraper — Reviews, Ratings and Listings by Location](https://apify.com) | Airbnb listings for a city or neighbourhood, screened by review count/rating, with guest reviews |
| [Walmart Reviews Scraper — Ratings, Seller & Variant Details](https://apify.com) | Walmart product reviews with the exact reviewed SKU/variant and seller/fulfillment details |

### How to extract Google Maps review data programmatically

The Actor runs as a standard Apify Actor: start it with one API call, poll or webhook for completion, and read back structured JSON from its dataset.

#### Python example

```python
from apify_client import ApifyClient

client = ApifyClient("<APIFY_API_TOKEN>")

run = client.actor("<ACTOR_ID>").call(run_input={
    "startUrls": ["https://www.google.com/maps/place/Boucherie+Union+Square/..."],
    "starRatings": ["1", "2"],
    "onlyWithoutOwnerReply": True,
    "maxReviews": 20,
})

for review in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(review["stars"], review["text"], review["reviewFilterSummary"])
```

#### Export to spreadsheets or CRM

Export the dataset as CSV or Excel from the Apify Console, or pull it via the Dataset API. Map `text` and `stars` to a ticket-body and priority column, `reviewUrl` to a "reply on Google" link, and `name`/`reviewerUrl` to a contact-name and contact-link field — no nested objects to unpack.

### Is it legal to scrape Google Maps?

Scraping publicly available Google Maps reviews is generally lawful — this Actor reads only reviews Google has already published on a place's public reviews tab, with no login and no private content accessed. Because most rows include personal data — a reviewer's name, contributor profile link, and avatar photo — GDPR and CCPA considerations apply to how you store and use that data, not to the act of reading a public review. Setting `personalData: false` removes `name`, `reviewerId`, `reviewerUrl`, and `reviewerPhotoUrl` from every row if you only need the review content and ratings. You are responsible for having a lawful basis for any storage or downstream use of personal data, and for complying with Google's own Terms of Service for your use case. Consult legal counsel for commercial applications involving bulk storage of personal data.

### ❓ FAQ

#### How does the Actor handle a review that's later deleted or edited on Google?

It doesn't retroactively re-check past runs — each run reads the place's live reviews feed at request time. If a review has since been deleted or edited, a fresh run simply returns whatever the feed currently shows; there is no separate "edited" or "deleted" flag on old rows.

#### Can I get the place's whole rating breakdown along with the complaint reviews?

Yes. `ratingDistribution` (the place's 1★–5★ histogram) and the derived `negativeReviewCount`/`negativeReviewShare` are attached to every row, read from the same payload behind Google's own reviews pane and cross-checked against the place's published `totalScore`. When that histogram isn't available for a place, all three fields come back `null` rather than a guessed value.

#### How accurate is the review date (`publishedAtDate`)?

Google only publishes a relative age per review — "3 months ago," never a calendar date — so `publishedAtDate` is derived from that bucket and is accurate to the bucket, not the day. `publishAt` preserves the exact string Google rendered. The derivation is verified against the English interface (`language: "en"`); other interface languages carry the same bucket-level accuracy.

#### How many reviews can I get per run?

Up to `maxReviews` matching reviews per place (maximum 1,000), found by reading up to `maxReviewsScanned` cards per place (maximum 3,000). Both are per-place limits, and `maxReviewsScanned` is raised automatically if you set it below `maxReviews` — you cannot hunt for more matches than you're allowed to read.

#### What's the difference between `reviewLanguages` and `language`?

`language` sets the interface language of the Maps pages the Actor fetches (`hl=<code>&gl=us`) — it changes how `publishAt`, `categories`, and `price` are rendered, but it does not filter which reviews come back. `reviewLanguages` is the actual content filter, matched against each review's own published language tag. Because Google front-loads a feed with reviews written in the selected interface language, the two interact in practice — set `language` to match the bulk of what you expect, then use `reviewLanguages` to narrow further.

#### Does Google Maps Reviews Scraper (Bad Review Finder) work with Claude, ChatGPT, and AI agent frameworks?

It is not published as an MCP server. It is callable as a standard Apify Actor run through the Apify API from any agent framework or HTTP client — including `apify_client` in Python or Node — so an agent can trigger a run and read the filtered dataset back as a tool call.

#### How does it compare to other Google Maps review scrapers?

As observed on their own Apify Store listings (checked 2026-08-16), the Compass and Scrapesmith Google Maps Reviews Scrapers both return individual review data with sort-order controls (newest, highest, lowest rating first), but neither exposes a dedicated star-rating filter, an unanswered-only filter, or a place-wide rating histogram — sorting by "lowest rating" still returns every review, so you pay for and store the positive reviews along with the negative ones. This Actor filters before charging and attaches `ratingDistribution`/`negativeReviewShare` to every row.

#### Can I use this without a Google API key or developer account?

Yes. No Google Cloud project, API key, or developer account is needed — only the Maps place URLs or Place IDs you want audited, entered directly into `startUrls` or `placeIds`. The Actor drives the public Maps site itself.

#### Why did a run return fewer rows than my `maxReviews`?

Either the place genuinely has fewer complaints than you asked for, or the feed hit `maxReviewsScanned` before finding enough matches. Every row carries `scannedReviewsCount`, `matchedReviewsCount`, and the five `droppedBy*Count` fields, so you can tell the two apart instead of guessing — and a place that read reviews but matched none is a real, reportable result, not a failed run: nothing is pushed and nothing is charged for it.

### Conclusion

Google Maps Reviews Scraper (Bad Review Finder) turns a place's public reviews tab into a pre-filtered complaint queue — the star ratings, written text, languages, and reply status you specify, with the place's whole rating mix attached to every row for context. It suits support teams working an actual reply backlog, agencies benchmarking clients on a schedule, and researchers mapping complaint density across a portfolio. Start a run from the Apify Console or trigger it via the Apify API with your list of Maps URLs or Place IDs to get your first complaint queue.

# Actor input Schema

## `startUrls` (type: `array`):

🚩 Paste one or more Google Maps place URLs (https://www.google.com/maps/place/...). Plain strings and {"url": "..."} objects both work.

## `placeIds` (type: `array`):

🆔 Each Place ID (`ChIJ...`) is turned into a Maps URL and audited alongside the URLs above.

## `starRatings` (type: `array`):

🚨 Which ratings belong in the complaint pile. `1` + `2` is the classic escalation queue; add `3` for the lukewarm ones. Selecting all five turns the star filter off. A card whose star widget could not be read is rejected and counted in `droppedByStarsCount` — it is never assumed to be negative.

## `requireWrittenText` (type: `boolean`):

✍️ Drop silent star-only ratings. There is nothing to reply to in a bare one-star click, and it inflates the queue. Rejections land in `droppedRatingOnlyCount`.

## `minTextLength` (type: `integer`):

📏 Skip reviews shorter than this. "Bad" with one word attached is still not something you can action. Set `0` to keep any text. Rejections land in `droppedByLengthCount`.

## `reviewLanguages` (type: `array`):

🌍 Keep only reviews whose own language tag is in this list (`en`, `es`, `de`, `zh-CN`…). Read from the review body's published language attribute, never guessed. ⚠️ A review Google did not tag is KEPT rather than guessed at, and counted in `unlabelledLanguageKeptCount`. ⚠️ Measured: Google front-loads the feed with reviews written in the interface `language` you selected (45/45 English at hl=en, 20/20 French at hl=fr on the same place, with zero overlap between the two sets) — so this filter mostly bites when it differs from `language`. Leave empty to keep every language.

## `onlyWithoutOwnerReply` (type: `boolean`):

📭 Keep only reviews that have no reply from the business yet — the actual work queue. Rejections land in `droppedByOwnerReplyCount`.

## `maxReviews` (type: `integer`):

✅ How many reviews that PASS the filter to keep per place. Unlike the plain scraper, this counts matches — the feed keeps scrolling until this many complaints exist or the scan budget below runs out.

## `maxReviewsScanned` (type: `integer`):

🔎 Hard ceiling on how many review cards may be READ per place (matching or not). This is the run-time dial: reading ~300 cards takes a few minutes. You are NOT charged for reviews that are read and rejected — only for matches. Raised automatically if it is below the match target.

## `reviewsSort` (type: `string`):

↕️ Order the Maps feed is read in. `lowestRanking` puts the complaints first and is by far the cheapest way to fill a 1-2★ queue; `newest` answers "what came in this week".

## `reviewsStartDateAbsolute` (type: `string`):

📅 Exact calendar date (YYYY-MM-DD). Google publishes only a relative age per review ("3 months ago"), so this cutoff is accurate to that published bucket, not to the day.

## `reviewsStartDateRelative` (type: `string`):

⏳ Used when the exact date above is empty. Same bucket-level accuracy.

## `language` (type: `string`):

🖥️ Language of the Maps pages that get scraped, applied as `hl=<code>&gl=us` in the page URL — the only mechanism Google honours. It changes localized output such as `publishAt`, `categories` and `price`. It does NOT filter reviews: use `reviewLanguages` for that. `publishedAtDate` is derived from the relative age and is most reliable in English.

## `personalData` (type: `boolean`):

🙈 When disabled, `name`, `reviewerId`, `reviewerUrl`, `reviewerPhotoUrl` and `reviewUrl` are emitted as null in every row. The complaint text, stars and dates are unaffected.

## `proxy` (type: `object`):

🛡️ Optional custom proxy. Set `use_proxy: true` with `host` and `port` to route the browser through your own IP; `username`/`password` may stay empty for unauthenticated proxies. Leave as-is to use Apify Residential (US), the default transport, which includes automatic IP rotation.

## Actor input object example

```json
{
  "startUrls": [
    "https://www.google.com/maps/place/Boucherie+Union+Square/@40.7372552,-73.9882246,17z/data=!4m6!3m5!1s0x89c259a1ec5f5573:0x2fc6687f46f682d5!8m2!3d40.7372552!4d-73.9882246!16s%2Fg%2F11hbv5rh0_"
  ],
  "placeIds": [],
  "starRatings": [
    "1",
    "2"
  ],
  "requireWrittenText": true,
  "minTextLength": 40,
  "reviewLanguages": [
    "en",
    "es"
  ],
  "onlyWithoutOwnerReply": false,
  "maxReviews": 10,
  "maxReviewsScanned": 300,
  "reviewsSort": "newest",
  "reviewsStartDateAbsolute": "",
  "reviewsStartDateRelative": "",
  "language": "en",
  "personalData": true,
  "proxy": {
    "use_proxy": false,
    "host": "",
    "port": 0,
    "username": "",
    "password": ""
  }
}
```

# Actor output Schema

## `results` (type: `string`):

All scraped items in the Actor's default dataset.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "startUrls": [
        "https://www.google.com/maps/place/Boucherie+Union+Square/@40.7372552,-73.9882246,17z/data=!4m6!3m5!1s0x89c259a1ec5f5573:0x2fc6687f46f682d5!8m2!3d40.7372552!4d-73.9882246!16s%2Fg%2F11hbv5rh0_"
    ],
    "placeIds": [],
    "reviewLanguages": [
        "en",
        "es"
    ],
    "proxy": {
        "use_proxy": false,
        "host": "",
        "port": 0,
        "username": "",
        "password": ""
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("simpleapi/google-maps-reviews-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "startUrls": ["https://www.google.com/maps/place/Boucherie+Union+Square/@40.7372552,-73.9882246,17z/data=!4m6!3m5!1s0x89c259a1ec5f5573:0x2fc6687f46f682d5!8m2!3d40.7372552!4d-73.9882246!16s%2Fg%2F11hbv5rh0_"],
    "placeIds": [],
    "reviewLanguages": [
        "en",
        "es",
    ],
    "proxy": {
        "use_proxy": False,
        "host": "",
        "port": 0,
        "username": "",
        "password": "",
    },
}

# Run the Actor and wait for it to finish
run = client.actor("simpleapi/google-maps-reviews-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "startUrls": [
    "https://www.google.com/maps/place/Boucherie+Union+Square/@40.7372552,-73.9882246,17z/data=!4m6!3m5!1s0x89c259a1ec5f5573:0x2fc6687f46f682d5!8m2!3d40.7372552!4d-73.9882246!16s%2Fg%2F11hbv5rh0_"
  ],
  "placeIds": [],
  "reviewLanguages": [
    "en",
    "es"
  ],
  "proxy": {
    "use_proxy": false,
    "host": "",
    "port": 0,
    "username": "",
    "password": ""
  }
}' |
apify call simpleapi/google-maps-reviews-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,simpleapi/google-maps-reviews-scraper"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/rkMmIeNlFQmRJTmFZ/builds/aNPr6tYE5zeAqVBgo/openapi.json
