# Google Maps Fresh Reviews (`cancap/google-maps-fresh-reviews`) Actor

Get only the newest Google Maps reviews for any business — never re-scrape history. Tracks each business's last-seen review automatically, so every run returns just the delta, sorted chronologically. Optional instant alerts (Slack, Zapier, or any webhook) when a new review comes in at 3★ or below.

- **URL**: https://apify.com/cancap/google-maps-fresh-reviews.md
- **Developed by:** [CANCAP](https://apify.com/cancap) (community)
- **Categories:** Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $10.00 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#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 Fresh Reviews Scraper

Apify Actor that extracts **only the newest reviews since the last run** for a
given Google Maps business, sorted chronologically. Built for reputation
monitoring — call it on a schedule and it returns just the delta each time.

### How it works

1. Opens the place URL, opens the Reviews panel, sets sort order to **Newest**.
2. Scrolls and reads review cards one batch at a time.
3. Stops as soon as it reaches a review it already saw on a previous run
   (tracked per-business in Apify's Key-Value Store) — it does **not**
   re-scrape the whole review history every time.
4. Pushes only the new reviews to the Dataset, and updates the "last seen"
   cutoff for next time.

### ⚠️ What has and hasn't been tested

This was built and unit-tested in a sandboxed environment with **no network
access to Google** (only npm/GitHub/PyPI-type registries are reachable there).
So:

- ✅ Syntax-checked, dependencies install cleanly, and the custom logic
  (relative-date parsing, CID/URL key extraction) is covered by
  `test/utils.test.js` — run `node test/utils.test.js` yourself to see it pass.
- ❌ **The actual browser-scraping flow (opening Maps, clicking Sort, reading
  review cards) has NOT been run against a live page.** It's written against
  Google's currently-documented DOM structure, but this is the part most
  likely to need a tweak on first real run.

**Do not skip the first test run** — deploy to Apify, run it against one
business you control or don't mind testing on, and check the Dataset output
before you point it at real customers.

### Deploying to Apify

1. Push this folder to a GitHub repo.
2. In the Apify Console: **Actors → Create new → Link Git repository**, point
   it at your repo.
3. Apify will build the Docker image automatically (it reads `.actor/actor.json`
   and the `Dockerfile`).
4. Set input, e.g.:
   ```json
   {
     "startUrls": [{ "url": "https://www.google.com/maps/place/.../@lat,lng,17z/data=...!4m5!3m4!1s0x...:0x...!8m2!3d...!4d..." }],
     "maxReviews": 100
   }
   ```
   Use the full Maps URL for the business (right-click the pin → "What's here?"
   or just copy the URL from your browser address bar when viewing the place).
5. Run it. Check the **Dataset** tab for output, and the **Key-Value Store**
   tab (`fresh-reviews-state`) to see the per-business cutoff being tracked.
6. Run it again a day or two later — on a business with no new reviews, the
   Dataset should come back empty. That's the actual "fresh reviews only"
   behavior working.

### When Google changes their DOM (selector maintenance)

Google's class names are obfuscated and do drift over time. If a run starts
returning empty/null fields where it used to return data, here's the fastest
way to fix it:

1. Open the business's Maps page in a normal Chrome browser, open Reviews.
2. Right-click a review's author name → **Inspect**.
3. Find the new class name and swap it into `src/main.js` — the relevant
   lines are clearly commented:
   ```js
   const authorName = await card.locator('div.d4r55')...       // author name
   const ratingLabel = await card.locator('span[role="img"]')... // star rating
   const relativeDate = await card.locator('span.rsqaWe')...    // "3 weeks ago"
   const reviewText = await card.locator('span.wiI7pd')...      // review body
   const ownerResponse = await card.locator('div.CDe7pd div.wiI7pd')... // owner reply
   ```
4. Push the fix, redeploy. Consider adding a scheduled daily smoke-test run
   on one known business so you catch breakage same-day.

### Files

```
.actor/actor.json         Apify actor manifest
.actor/input_schema.json  Input form shown in Apify Console
Dockerfile                Apify Playwright+Chrome base image
package.json              Dependencies (apify, crawlee, playwright)
src/main.js               Main scraping logic
src/utils.js               Date parsing + place-key extraction (unit-tested)
src/webhook.js             Low-rating alert webhook sender (unit-tested)
test/utils.test.js        Standalone tests for the logic above
test/webhook.test.js      Tests the webhook sender against a real local HTTP server
```

### Low-rating alert webhook

Set `webhookUrl` in the input to get a POST request whenever new reviews at
or below `lowRatingThreshold` (default 3★) show up. This is the actual
reputation-monitoring differentiator -- without it, this is "just" a
scraper.

- **Slack**: paste a Slack Incoming Webhook URL directly into `webhookUrl`.
  The payload includes a `text` field Slack reads automatically.
- **Zapier / Make.com / your own endpoint**: point `webhookUrl` at your
  trigger URL. The full payload (place info, threshold, and the complete
  review objects) is included as JSON alongside `text`.
- Only fires when there's at least one matching review in that run, and a
  failed webhook delivery is logged as a warning -- it never fails the scrape
  itself.

### Suggested next features (not built yet)

- Owner-response-missing flag (reviews with no reply after N days).
- Scheduled runs via Apify's built-in Scheduler (no code needed -- just
  point a Schedule at this actor with your business list as input).

# Actor input Schema

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

One or more Google Maps business URLs (share link or full maps.google.com URL with ?cid=...). One business per entry.

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

Safety cap. The actor stops early once it reaches a review it has already seen, even if this number isn't hit. Kept low by default because Google lazy-loads reviews with real network delay between batches -- raise this once you've confirmed a run completes comfortably within your Apify run timeout (Input > Run options).

## `sortBy` (type: `string`):

Newest is required for the 'fresh reviews only' logic to work correctly.

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

Residential proxies are strongly recommended for reliability. Datacenter proxies get blocked quickly on Google Maps.

## `webhookUrl` (type: `string`):

If set, a POST request fires whenever new reviews at or below the threshold below are found. Works directly with a Slack Incoming Webhook URL (reads the 'text' field), or any generic endpoint (Zapier, Make.com, your own server) that can read the full JSON payload.

## `lowRatingThreshold` (type: `integer`):

New reviews at or below this star rating trigger the webhook above. Ignored if no webhook URL is set.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://www.google.com/maps/place/?q=place_id:ChIJExampleReplaceMe"
    }
  ],
  "maxReviews": 30,
  "sortBy": "newest",
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  },
  "lowRatingThreshold": 3
}
```

# 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": [
        {
            "url": "https://www.google.com/maps/place/?q=place_id:ChIJExampleReplaceMe"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("cancap/google-maps-fresh-reviews").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": [{ "url": "https://www.google.com/maps/place/?q=place_id:ChIJExampleReplaceMe" }] }

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

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).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": [
    {
      "url": "https://www.google.com/maps/place/?q=place_id:ChIJExampleReplaceMe"
    }
  ]
}' |
apify call cancap/google-maps-fresh-reviews --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=cancap/google-maps-fresh-reviews",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/acts/FXX5LORxe4ZDRNKiV/builds/rlRq98pu4x8rJnpJv/openapi.json
