# Google Maps Reviews Scraper with Sentiment & Replies (`leadsbrary/google-maps-reviews-reputation-tracker`) Actor

Scrape Google Maps reviews with star rating, text, date, reviewer name, owner-reply detection, reply latency in days and star-anchored sentiment. Filter unanswered or by star rating. Export to JSON, CSV or Excel. From $0.18 per 1,000 results.

- **URL**: https://apify.com/leadsbrary/google-maps-reviews-reputation-tracker.md
- **Developed by:** [Alexandre Manguis](https://apify.com/leadsbrary) (community)
- **Categories:** Lead generation, SEO tools, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.18 / 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.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Google Maps Reviews Scraper with Sentiment & Owner-Reply Tracking

Scrape Google Maps reviews for any public place and get them back reputation-ready: star rating, text, date, reviewer info, **owner-reply detection, reply latency in days, star-anchored sentiment, and a `needsResponse` flag** — no post-processing code required. Export to JSON, CSV or Excel.

### What it does

This Actor collects the publicly visible reviews shown on a Google Maps place page (one or many places per run) and, for each review, computes the fields a reputation-management workflow actually needs:

- Whether the owner replied, and how long it took (`ownerReplied`, `ownerReplyText`, `replyLatencyDays`). Google exposes no exact timestamp for replies, only a coarse relative label (e.g. "a week ago"); `replyLatencyDays` is therefore an approximation whose error grows with the reply's age, and `null` when it can't be placed reliably.
- A sentiment label and score derived from the star rating (plus a lexicon fallback for common English words), so you don't need to call an LLM or a sentiment API per review.
- A `needsResponse` flag: `true` when a review is negative/neutral **and** unanswered — the exact triage filter an agency needs.
- Server-side filters (`onlyUnanswered`, `minStars`/`maxStars`, `reviewsNewerThan`) so you pay only for the reviews that matter to you.

Most low-cost Google Maps review scrapers return the raw star/text/date dump and leave sentiment, reply-matching and latency to you. This Actor does that work locally, for free, as part of the same run.

Each run returns up to **60 of the newest reviews per place** (Google's own review-list response is capped at 60 regardless of what you request — see FAQ). This is designed for recurring reputation monitoring (schedule a run + `reviewsNewerThan`), not a one-off full historical export.

### Who is it for / use cases

- **Reputation-management & local-SEO agencies** tracking several client locations who need a weekly report: rating trend, sentiment mix, unanswered count, median reply latency.
- **Multi-location businesses** (restaurant, retail, clinic chains) that want to list every negative review with `ownerReplied=false` older than N days, per location — an SLA tracking view.
- **AI reply-drafting / RAG pipeline builders** who need clean, sentiment-tagged, reply-aware review data to feed an agent that drafts owner responses.
- **Competitive benchmarking**: compare sentiment mix and reply responsiveness of a business against nearby competitors' public reviews.

### Features

- Collects reviews from one or many Google Maps place URLs in a single run.
- Sort by newest, most relevant, highest or lowest rating.
- Filter by star range, text-only reviews, unanswered reviews, or a minimum publish date (`reviewsNewerThan`) — filtering happens before billing, so filtered-out rows are never charged.
- Owner-reply detection with reply latency in days (approximate — see note above, Google gives no exact timestamp for replies).
- Local, rule-based sentiment (`positive`/`neutral`/`negative`) anchored on the star rating, with a documented lexicon fallback — no external API cost, no per-review charge.
- Data-minimisation default: reviewer profile URL and contribution count are `null` unless you explicitly opt in (`includeReviewerProfile`).
- Structured skip rows (`skipped: true`, `skipReason`, no billing event) for invalid URLs or places with zero reviews — the run still succeeds.

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `placeUrls` | array of string (required) | — | Google Maps place URLs to collect reviews from. Accepts both the `?q=place_id:...` form and the `@lat,lng/data=...!1s0x...` form. |
| `maxReviewsPerPlace` | integer | `50` | Max reviews collected per place, **capped at 60**: Google's review-list endpoint returns at most 60 reviews per response regardless of this value, and its pagination token does not work over plain HTTP (see FAQ). |
| `maxResults` | integer | `200` | Hard cap on total reviews pushed to the dataset across all places. |
| `reviewsSort` | enum: `newest`, `relevant`, `highestRanking`, `lowestRanking` | `newest` | Sort order requested from Google. |
| `reviewsNewerThan` | string | — | Skip reviews older than this date (ISO date like `2026-09-01`, or relative like `30 days`). Pagination stops early when `reviewsSort=newest`. |
| `minStars` / `maxStars` | integer 1–5 | — | Keep only reviews within this star range. |
| `onlyUnanswered` | boolean | `false` | Keep only reviews with no public owner response. |
| `onlyWithText` | boolean | `false` | Skip star-only reviews with no text. |
| `language` | string (ISO 639-1) | `en` | Interface/response language requested from Google. |
| `computeSentiment` | boolean | `true` | Compute sentiment locally. Disable for a pure raw dump. |
| `includeReviewerProfile` | boolean | `false` | Include the reviewer's public profile URL and contribution count. Off by default for GDPR data minimisation. |
| `proxyConfiguration` | object | `{"useApifyProxy": true}` | Apify Proxy settings. Datacenter proxy is sufficient for this target; escalate to residential only if you see repeated blocks. |

### Output (real sample)

```json
{
  "placeUrl": "https://www.google.com/maps/place/?q=place_id:ChIJrTLr-GyuEmsRBfy61i59si0",
  "placeId": "ChIJrTLr-GyuEmsRBfy61i59si0",
  "skipped": false,
  "skipReason": null,
  "reviewId": "Ci9DQUlRQUNvZENodHljRjlvT2xWdVUzTXlTbUY0U1hwSFF6SkNNbWx2YW1sdFZrRRAB",
  "reviewUrl": "https://www.google.com/maps/reviews/data=!4m8!14m7!1m6!2m5!1sCi9DQUlRQUNvZENodHljRjlvT2xWdVUzTXlTbUY0U1hwSFF6SkNNbWx2YW1sdFZrRRAB...",
  "stars": 4,
  "text": "Great service and experience. I wish the menu was less focused on sea food and a bit more variety. Recommended.",
  "textLanguage": "en",
  "publishedAtDate": "2026-07-06T09:19:28.897Z",
  "publishedAtText": "2 months ago",
  "daysSincePublished": 88,
  "likesCount": 0,
  "reviewerName": "Raph",
  "reviewerIsLocalGuide": false,
  "reviewerReviewsCount": null,
  "reviewerProfileUrl": null,
  "ownerReplied": true,
  "ownerReplyText": "Thanks so much for your kind words! We're delighted you had a great experience with us. We also appreciate your feedback about the menu; we'll certainly pass on to our team for consideration.",
  "ownerReplyPublishedAtDate": "2026-08-03T09:58:34.381Z",
  "replyLatencyDays": 28,
  "sentiment": "positive",
  "sentimentScore": 0.45,
  "sentimentMethod": "stars+lexicon",
  "needsResponse": false,
  "scrapedAt": "2026-10-02T09:58:34.381Z"
}
```

`ownerReplyPublishedAtDate` and `replyLatencyDays` are derived from Google's relative label for the reply ("2 weeks ago" etc.) — Google exposes no exact timestamp for owner replies. The error grows with the reply's age, and both fields are `null` when the label can't be resolved to a date at or after the review itself.

(This row is the first row of `test/production-sample.json`, a real recorded run committed in the repo, and is shown verbatim — `reviewerName` is the public display name exactly as Google Maps shows it, here a single-word handle. The same file proves `ownerReplied`/`ownerReplyText`/`ownerReplyPublishedAtDate`/`replyLatencyDays` end-to-end: 46 of its 61 rows have `ownerReplied: true`, and on a 60-review sample from the same place `replyLatencyDays` resolved for 22/22 answered reviews — see `owner_reply_detection` in `TEST_REPORT.json`.)

A skip row for an invalid URL or a place with zero public reviews looks like this instead (never billed, taken verbatim from `test/production-sample.json`):

```json
{
  "placeUrl": "https://example.com/not-a-google-maps-url",
  "placeId": null,
  "skipped": true,
  "skipReason": "not a Google Maps place URL",
  "scrapedAt": "2026-10-02T09:58:31.146Z"
}
```

### How to use via API

#### curl

```bash
curl "https://api.apify.com/v2/acts/YOUR_USERNAME~google-maps-reviews-reputation-tracker/run-sync-get-dataset-items?token=YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "placeUrls": ["https://www.google.com/maps/place/?q=place_id:ChIJN1t_tDeuEmsRUsoyG83frY4"],
    "maxReviewsPerPlace": 50,
    "maxResults": 200,
    "onlyUnanswered": true,
    "maxStars": 3
  }'
```

#### JavaScript (apify-client)

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

const client = new ApifyClient({ token: 'YOUR_API_TOKEN' });

const run = await client.actor('YOUR_USERNAME/google-maps-reviews-reputation-tracker').call({
  placeUrls: ['https://www.google.com/maps/place/?q=place_id:ChIJN1t_tDeuEmsRUsoyG83frY4'],
  maxReviewsPerPlace: 50,
  maxResults: 200,
  onlyUnanswered: true,
  maxStars: 3,
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items);
```

#### Python (apify-client)

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_API_TOKEN")

run_input = {
    "placeUrls": ["https://www.google.com/maps/place/?q=place_id:ChIJN1t_tDeuEmsRUsoyG83frY4"],
    "maxReviewsPerPlace": 50,
    "maxResults": 200,
    "onlyUnanswered": True,
    "maxStars": 3,
}

run = client.actor("YOUR_USERNAME/google-maps-reviews-reputation-tracker").call(run_input=run_input)

for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)
```

### Pricing

Pay-per-result (`PAY_PER_EVENT`), billed as one **Result** event per review actually pushed to the dataset. Reviews removed by your own filters (`onlyUnanswered`, `minStars`/`maxStars`, `reviewsNewerThan`, invalid/empty places) are never charged.

| Tier | Price per 1,000 reviews |
|---|---|
| Free | $0.27 |
| Bronze | $0.23 |
| Silver | $0.21 |
| Gold | $0.18 |
| Platinum | $0.18 |
| Diamond | $0.18 |

This undercuts the niche leader (compass/Google-Maps-Reviews-Scraper, $0.30/1000 Gold) while adding sentiment, reply latency and the `needsResponse` triage flag at no extra cost.

### Legal & privacy

This Actor only reads what Google Maps already shows to any anonymous visitor on a public place page — no login, no paywall, no access-control bypass, no CAPTCHA-solving. Reviewer names are personal data under GDPR even when publicly displayed; by default the Actor does not fetch the reviewer's profile URL or contribution count (`includeReviewerProfile=false`), performs no enrichment, and does no cross-source identity resolution. You, the Apify user running this Actor, are the data controller for any dataset you export — please handle it under your own GDPR obligations. Google's Maps terms discourage bulk download/caching of Maps content; this is a contractual risk for the party scraping, not a technical access-control violation (no account is used or at risk here), and the practice is widely operated on Apify. Decide knowingly.

### Troubleshooting

- **Empty or very few results for a place**: the place may have fewer reviews than `maxReviewsPerPlace`, or your filters (`minStars`/`maxStars`/`onlyUnanswered`/`reviewsNewerThan`) excluded the rest. Check the dataset for a `skipped: true` row with a `skipReason`, or the run log.
- **A place/URL never appears in the output**: look for its `skipped: true` row — `skipReason` explains why (invalid URL, zero public reviews, a fetch error, or all reviews removed by your own filters); skip rows are never billed.
- **Repeated HTTP 429 / "unusual traffic"**: switch `proxyConfiguration` from datacenter to residential proxy groups; Google is rate-limiting the datacenter IP range for that place.
- **`replyLatencyDays` is `null` even though `ownerReplied` is `true`**: either the owner reply's relative label (Google gives no exact timestamp for replies, see below) couldn't be parsed, or it resolved to a date before the review itself — too coarse to place reliably, so the Actor drops it to `null` instead of publishing an impossible date pair.
- **Non-English reviews show `sentimentMethod: "stars"` instead of `"stars+lexicon"`**: the sentiment lexicon only covers common English words; other languages fall back to the star-anchored method, which is still returned, just less text-aware.
- **Log line `reviewsSort="newest" came back empty; fell back to sort="relevant"`**: for a small number of places Google's own `newest`-sorted endpoint returns nothing even though the place has reviews; the Actor retries once with `relevant` so you still get results instead of an empty place. Results for that place are not ordered the way you requested, but `reviewsNewerThan` is still applied correctly per review (no reviews are silently dropped by the pagination cutoff).

### FAQ

**Does this use the official Google Places API?**
No. The Places API caps review results at roughly 5 per place, which isn't enough for reputation monitoring. This Actor reads the same public review data Google serves to the Maps website.

**Is the sentiment analysis AI-based?**
No — it's a transparent, rule-based method anchored on the star rating with an English lexicon fallback (`sentimentMethod` tells you which rule produced the label). This keeps the Actor fast and free of per-review API cost. Disable it with `computeSentiment=false` for a pure raw dump.

**Can I monitor the same places on a schedule?**
Yes — combine `reviewsSort=newest` with `reviewsNewerThan` (e.g. `"7 days"`) on an Apify Schedule to pull only new reviews each run and keep cost low.

**Will I be charged for reviews filtered out by `onlyUnanswered` or star filters?**
No — filtering happens before the row is pushed to the dataset, and billing only happens on pushed rows.

**How many reviews per place can I get?**
Up to 60 newest reviews per place per run — Google's review-list response caps out at 60 regardless of the requested page size, and the pagination token it returns consistently comes back empty over plain HTTP. This Actor is built for recurring monitoring (schedule + `reviewsNewerThan`) rather than a one-time full historical backfill of a place with thousands of reviews.

**What happens to an invalid place URL or a place with zero reviews?**
It gets a structured skip row (`skipped: true`, `skipReason` explaining why) instead of failing the run or being silently dropped, and it is never billed.

### Keywords

google maps reviews scraper, google reviews sentiment analysis, google maps reputation tracker, review response tracker, unanswered google reviews, owner reply latency, google reviews api alternative, local seo review monitoring, multi location review export, google business profile reviews export, review sentiment csv export, scrape google reviews without api key

### Hashtags

\#GoogleMaps #GoogleReviews #ReviewScraper #ReputationManagement #LocalSEO #SentimentAnalysis #LeadGeneration #WebScraping #Apify #CustomerFeedback #MultiLocation

# Changelog

This Actor's version history is a separate document: https://apify.com/leadsbrary/google-maps-reviews-reputation-tracker/changelog.md

# Actor input Schema

## `placeUrls` (type: `array`):

Google Maps place URLs whose public reviews to collect (accepts both the `?q=place_id:...` form and the `@lat,lng/data=...!1s0x...` form). At least one is required.

## `maxReviewsPerPlace` (type: `integer`):

Maximum reviews collected per place. Google's review-list endpoint returns at most 60 reviews in a single response regardless of this value (a confirmed server-side cap — see README), so this is also the practical ceiling per place per run.

## `maxResults` (type: `integer`):

Hard cap on total reviews pushed to the dataset across all places; the run stops cleanly once reached (also protects your spending).

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

Sort order requested from Google. 'newest' is the default because reputation monitoring is time-based.

## `reviewsNewerThan` (type: `string`):

Skip reviews published before this date (ISO date, e.g. 2026-09-01, or '<N> days', e.g. '30 days'). Pagination stops early when reviewsSort=newest, lowering cost for recurring monitoring runs.

## `minStars` (type: `integer`):

Keep only reviews with at least this many stars.

## `maxStars` (type: `integer`):

Keep only reviews with at most this many stars — e.g. 3 to triage detractors.

## `onlyUnanswered` (type: `boolean`):

Keep only reviews with no public owner response.

## `onlyWithText` (type: `boolean`):

Skip star-only reviews with no text (nothing to analyse or answer).

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

Interface/response language requested from Google (ISO 639-1, e.g. 'en', 'fr', 'es'). Affects relative date strings and owner-reply labels.

## `computeSentiment` (type: `boolean`):

Compute sentiment label/score locally (star-anchored rules, no external API, no extra cost). Disable for a pure raw dump.

## `includeReviewerProfile` (type: `boolean`):

Include the reviewer's public profile URL and contribution count. Off by default for data minimisation (GDPR).

## `proxyConfiguration` (type: `object`):

Apify Proxy settings. Datacenter proxy is proven sufficient for this target; escalate to residential only if you see repeated blocks.

## Actor input object example

```json
{
  "placeUrls": [
    "https://www.google.com/maps/place/?q=place_id:ChIJN1t_tDeuEmsRUsoyG83frY4"
  ],
  "maxReviewsPerPlace": 10,
  "maxResults": 20,
  "reviewsSort": "newest",
  "onlyUnanswered": false,
  "onlyWithText": false,
  "language": "en",
  "computeSentiment": true,
  "includeReviewerProfile": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

No description

# 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 = {
    "placeUrls": [
        "https://www.google.com/maps/place/?q=place_id:ChIJN1t_tDeuEmsRUsoyG83frY4"
    ],
    "maxReviewsPerPlace": 10,
    "maxResults": 20,
    "reviewsSort": "newest",
    "language": "en",
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("leadsbrary/google-maps-reviews-reputation-tracker").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 = {
    "placeUrls": ["https://www.google.com/maps/place/?q=place_id:ChIJN1t_tDeuEmsRUsoyG83frY4"],
    "maxReviewsPerPlace": 10,
    "maxResults": 20,
    "reviewsSort": "newest",
    "language": "en",
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("leadsbrary/google-maps-reviews-reputation-tracker").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 '{
  "placeUrls": [
    "https://www.google.com/maps/place/?q=place_id:ChIJN1t_tDeuEmsRUsoyG83frY4"
  ],
  "maxReviewsPerPlace": 10,
  "maxResults": 20,
  "reviewsSort": "newest",
  "language": "en",
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call leadsbrary/google-maps-reviews-reputation-tracker --silent --output-dataset

```

## MCP server setup

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

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/vxZByQY0ZjKh03zMB/builds/jSAjwQtPPVOMo7fSe/openapi.json
