# Google Maps Business Change Monitor (`coregent/google-maps-business-change-monitor`) Actor

Monitor Google Maps businesses over time and get only meaningful listing changes: closures, reopenings, opening-hours changes, rating and review-count movement, phone, website, address and category updates. Works from Maps URLs, Place IDs, CIDs or business names, with state kept between runs.

- **URL**: https://apify.com/coregent/google-maps-business-change-monitor.md
- **Developed by:** [Delowar Munna](https://apify.com/coregent) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 1,000 business place checks

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/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 Business Change Monitor

![Google Maps Business Change Monitor](https://raw.githubusercontent.com/coregentdevspace/google-maps-business-change-monitor-assets/main/thumbnail-google-maps-business-change-monitor.png)

**Track Google Maps business listings over time and get only the changes that matter** — closures and reopenings, opening-hours changes, rating and review-count movement, phone, website, address and category updates, attributes, claim status and (optionally) Popular Times.

Give it Google Maps URLs, Place IDs, CIDs or business names. It fetches each place live, compares it with the state it stored on the previous run, and returns **one row per detected change**. Schedule it daily or weekly and connect a webhook — you get alerts, not spreadsheets to diff.

***

### ✨ What it does

- **Live place retrieval built in** — no need to run a scraper first and pass two datasets.
- **Persistent state** — the Actor remembers every business between runs (per *monitor key*).
- **Only new changes** — a business that didn't change produces no change rows.
- **First run = baseline** — the first check of a place stores its state; it never floods you with "field added" events.
- **Field-level events** with `severity`, `confidence`, previous and current values, and structured details (e.g. which day's closing time moved).
- **Safe by design** — a field missing from one read is *never* reported as removed (see [Missing data](#-how-are-missing-fields-handled)).
- **History** — period comparisons (7 / 30 / 90 days …) and an append-only history dataset.
- **Alerts-ready** — every row carries `alertTriggered` and `alertReasons` for Slack, email, Make, Zapier, n8n or your CRM.

### 🆚 How is this different?

|                         | Google Maps place scrapers             | Google Maps Reviews Scraper | **Business Change Monitor**            |
| ----------------------- | -------------------------------------- | --------------------------- | -------------------------------------- |
| Question it answers     | What does this business look like now? | What reviews does it have?  | **What changed since the last check?** |
| Output                  | One row per place                      | One row per review          | **One row per change**                 |
| Remembers previous runs | No                                     | No                          | **Yes**                                |

**Compared with dataset-comparison tools** (including another Store Actor with a similar name): those compare two datasets *you* supply and return a report. This Actor retrieves the places itself, keeps the history for you, and returns only what is new since the last run — you just schedule it.

### 🚀 Quick start

```json
{
    "places": ["ChIJ3S-JXmauEmsRUcIaWtf4MzE"],
    "monitorKey": "competitors"
}
```

1. **Run 1** creates the baseline: one summary row per business with `status: "BASELINE_CREATED"`.
2. **Schedule it** (daily is a good default). Each later run returns a change row for everything that changed, plus a free summary row per business (`NO_CHANGES` or `SUCCESS`).

#### Sydney landmarks (the input behind the sample output below)

```json
{
    "places": [
        "ChIJ3S-JXmauEmsRUcIaWtf4MzE",
        "ChIJ49XqJV2uEmsRPsTAF7eOlGg",
        "ChIJq6qqWiSsEmsRJuIpepyEua4",
        "ChIJx4FyRJytEmsReOktxgkYwyA",
        "ChIJWaTdYGuuEmsRoOfx-Wh9AQ8",
        "ChIJy6BwuD-uEmsRyiRskH1UG-I",
        "ChIJYV-J-ziuEmsRIMyoFaMedU4",
        "ChIJ384_FGCuEmsRFAbHLAWVPGg",
        "ChIJPVqlfGyuEmsRHPcnCX1X1OE",
        "ChIJlwsH0RWuEmsR3Cg3WEDw76I",
        "ChIJK8ybIQmrEmsRUHjYur4mf6k"
    ],
    "monitorKey": "readme-sydney-landmarks",
    "mode": "MONITOR",
    "comparisonPeriods": ["PREVIOUS", "7_DAYS", "30_DAYS"],
    "includeAttributes": true,
    "includeProfileSummary": true
}
```

#### Daily competitor monitor

```json
{
    "places": [
        "https://www.google.com/maps/place/?q=place_id:ChIJNUOUhSKuEmsRj2F2Arcx_OQ",
        "cid:1787461144199566946",
        "Rockpool Bar & Grill, Sydney"
    ],
    "monitorKey": "restaurant-competitors",
    "mode": "MONITOR",
    "trackFields": ["STATUS", "OPENING_HOURS", "RATING", "REVIEW_COUNT", "PHONE", "WEBSITE"]
}
```

#### Local SEO clients (NAP + categories + hours)

```json
{
    "places": ["ChIJNUOUhSKuEmsRj2F2Arcx_OQ", "ChIJo8ZIgT6uEmsRYhbUtXBWzhg"],
    "monitorKey": "local-seo-clients",
    "trackFields": ["NAME", "CATEGORIES", "ADDRESS", "PHONE", "WEBSITE", "OPENING_HOURS", "RATING", "REVIEW_COUNT"]
}
```

#### Franchise locations from an existing dataset, compared with 30 and 90 days ago

```json
{
    "datasetId": "<your Google Maps dataset ID>",
    "monitorKey": "franchise-locations",
    "mode": "COMPARE",
    "comparisonPeriods": ["30_DAYS", "90_DAYS"]
}
```

#### Popular Times watch

```json
{
    "places": ["ChIJo8ZIgT6uEmsRYhbUtXBWzhg"],
    "monitorKey": "footfall-watch",
    "includePopularTimes": true,
    "minimumPopularTimesChange": 15
}
```

### 📥 Input

| Field                        | Default                              | What it does                                                                                                                 |
| ---------------------------- | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
| `places`                     | sample place                         | Maps URLs, share links, Place IDs (`ChIJ…`), CIDs (`cid:123…` or digits), feature IDs (`0x…:0x…`) or `"Business name, City"` |
| `datasetId`                  | —                                    | Optional existing dataset; each row's `placeId` / `cid` / `featureId` / Maps URL is monitored                                |
| `maxPlaces`                  | 1000                                 | Upper bound per run                                                                                                          |
| `monitorKey`                 | `default`                            | Name of the monitor — keeps independent state per project                                                                    |
| `mode`                       | `MONITOR`                            | `MONITOR` (only new changes), `BASELINE` (store state, no changes), `COMPARE` (against past periods)                         |
| `comparisonPeriods`          | `PREVIOUS`, `7_DAYS`, `30_DAYS`      | COMPARE: one set of events per period. MONITOR: added as period summaries                                                    |
| `trackFields`                | all core fields                      | Which fields produce events (main photo is opt-in)                                                                           |
| `includePopularTimes`        | `false`                              | Also monitor Popular Times + typical visit duration (slower, add-on event)                                                   |
| `includeAttributes`          | `true`                               | Track public attributes (e.g. wheelchair access)                                                                             |
| `includeProfileSummary`      | `true`                               | One free summary row per business                                                                                            |
| `emitBaseline`               | `false`                              | Add one `BASELINE_CREATED` row on a place's first check                                                                      |
| `minimumRatingChange`        | `0.1`                                | Smallest rating move that counts                                                                                             |
| `minimumReviewCountChange`   | `1`                                  | Smallest review-count move that counts                                                                                       |
| `minimumPopularTimesChange`  | `10`                                 | Percentage points per hourly slot                                                                                            |
| `minimumLocationShiftMeters` | `50`                                 | Smaller pin moves are ignored                                                                                                |
| `saveHistory`                | `true`                               | Keep snapshots for period comparisons + history dataset                                                                      |
| `maxHistoryDays`             | `365`                                | How far back snapshots are kept                                                                                              |
| `stateStoreName`             | `google-maps-business-monitor-state` | Named store holding the state                                                                                                |
| `language`                   | `en`                                 | Google Maps language — keep it constant across runs                                                                          |

### 📤 Output

The default dataset has two views: **Changes** (one row per change event) and **Businesses** (one free summary row per business).

![Output in the Apify Console — table view](https://raw.githubusercontent.com/coregentdevspace/google-maps-business-change-monitor-assets/main/google-maps-business-change-monitor-output-all-fields-table-view.png)

*First run of the Sydney landmarks input above: one `BASELINE_CREATED` summary row per business, all `HIGH` snapshot quality. Change rows appear from the second run on.*

#### Change row (`rowType: "business_change"`)

A real change from a scheduled re-check of Sydney Opera House — four new reviews since the previous run:

```json
{
    "rowType": "business_change",
    "monitorKey": "default",
    "eventId": "02b91b3e063f69e0dffb",
    "eventType": "REVIEW_COUNT_CHANGED",
    "severity": "INFO",
    "confidence": "HIGH",
    "business": {
        "placeId": "ChIJ3S-JXmauEmsRUcIaWtf4MzE",
        "cid": "3545450935484072529",
        "featureId": "0x6b12ae665e892fdd:0x3133f8d75a1ac251",
        "name": "Sydney Opera House",
        "googleMapsUrl": "https://www.google.com/maps/place/?q=place_id:ChIJ3S-JXmauEmsRUcIaWtf4MzE&hl=en",
        "address": "Bennelong Point, Sydney NSW 2000, Australia"
    },
    "change": {
        "field": "reviewCount",
        "previousValue": 93519,
        "currentValue": 93523,
        "previousReviewCount": 93519,
        "currentReviewCount": 93523,
        "reviewCountDelta": 4,
        "newReviewsPerDay": 8.54,
        "baselineReviewsPerDay": null
    },
    "signals": ["NEW_REVIEWS_ADDED"],
    "comparisonPeriod": "PREVIOUS",
    "previousObservedAt": "2026-10-23T14:03:25.304Z",
    "detectedAt": "2026-10-24T01:18:14.239Z",
    "snapshotQuality": "HIGH",
    "alertTriggered": false,
    "alertReasons": [],
    "input": "ChIJ3S-JXmauEmsRUcIaWtf4MzE",
    "status": "SUCCESS"
}
```

Other event types use the same row shape. An opening-hours change lists every changed day in `change.details` (shape shown with illustrative values):

```json
{
    "eventType": "OPENING_HOURS_CHANGED",
    "severity": "MEDIUM",
    "change": {
        "field": "openingHours",
        "previousValue": { "Monday": ["07:00-14:00"], "Sunday": [] },
        "currentValue": { "Monday": ["07:00-15:30"], "Sunday": ["08:00-15:00"] },
        "details": [
            { "day": "Monday", "type": "CLOSE_TIME_CHANGED", "previous": "14:00", "current": "15:30" },
            { "day": "Sunday", "type": "DAY_BECAME_OPEN", "previous": [], "current": ["08:00-15:00"] }
        ]
    },
    "alertTriggered": true,
    "alertReasons": ["HOURS_CHANGED"]
}
```

A rating change carries the numbers directly (illustrative values):

```json
{
    "eventType": "RATING_CHANGED",
    "severity": "MEDIUM",
    "change": {
        "field": "rating",
        "previousRating": 4.7,
        "currentRating": 4.5,
        "ratingDelta": -0.2,
        "previousReviewCount": 3270,
        "currentReviewCount": 3310,
        "reviewCountDelta": 40
    },
    "signals": ["RATING_DECLINING", "NEW_REVIEWS_ADDED"],
    "alertReasons": ["RATING_DROP"]
}
```

#### Event types

| Field               | Events                                                                                                                                                                                                                | Severity                  |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- |
| Status              | `BUSINESS_PERMANENTLY_CLOSED`                                                                                                                                                                                         | CRITICAL                  |
|                     | `BUSINESS_TEMPORARILY_CLOSED`, `BUSINESS_REOPENED`                                                                                                                                                                    | HIGH                      |
| Name                | `NAME_CHANGED`                                                                                                                                                                                                        | MEDIUM                    |
| Categories          | `PRIMARY_CATEGORY_CHANGED` · `CATEGORY_ADDED` / `CATEGORY_REMOVED`                                                                                                                                                    | MEDIUM · LOW              |
| Address             | `ADDRESS_CHANGED`, `BUSINESS_RELOCATED` (address changed **and** pin moved)                                                                                                                                           | HIGH                      |
| Map pin             | `LOCATION_PIN_CHANGED` (pin moved, address unchanged)                                                                                                                                                                 | LOW                       |
| Phone               | `PHONE_CHANGED` · `PHONE_ADDED` · `PHONE_REMOVED`                                                                                                                                                                     | MEDIUM · LOW · HIGH       |
| Website             | `WEBSITE_CHANGED` · `WEBSITE_ADDED` · `WEBSITE_REMOVED`                                                                                                                                                               | MEDIUM · LOW · HIGH       |
| Opening hours       | `OPENING_HOURS_CHANGED` (details: `DAY_BECAME_CLOSED`, `DAY_BECAME_OPEN`, `OPEN_TIME_CHANGED`, `CLOSE_TIME_CHANGED`, `INTERVALS_CHANGED`) · `OPENING_HOURS_ADDED` · `OPENING_HOURS_REMOVED` · `SPECIAL_HOURS_CHANGED` | MEDIUM · LOW · HIGH · LOW |
| Rating              | `RATING_CHANGED` (signals `RATING_DECLINING` / `RATING_IMPROVING`)                                                                                                                                                    | MEDIUM down · LOW up      |
| Review count        | `REVIEW_COUNT_CHANGED` (signals `NEW_REVIEWS_ADDED`, `REVIEW_COUNT_DROPPED`, `REVIEW_VELOCITY_SPIKE`)                                                                                                                 | INFO up · MEDIUM down     |
| Price level         | `PRICE_LEVEL_CHANGED`                                                                                                                                                                                                 | LOW                       |
| Claim status        | `BUSINESS_CLAIMED` · `BUSINESS_UNCLAIMED`                                                                                                                                                                             | LOW · MEDIUM              |
| Attributes          | `ATTRIBUTE_ADDED` · `ATTRIBUTE_REMOVED`                                                                                                                                                                               | INFO · LOW                |
| Popular Times       | `POPULAR_TIMES_CHANGED` (slot details + busiest day/hour + average delta)                                                                                                                                             | LOW                       |
| Visit duration      | `TYPICAL_VISIT_DURATION_CHANGED`                                                                                                                                                                                      | INFO                      |
| Main photo (opt-in) | `MAIN_PHOTO_CHANGED`                                                                                                                                                                                                  | INFO                      |

`alertTriggered` is `true` for every CRITICAL/HIGH event and for hours changes, rating drops, name changes and unclaimed listings. `alertReasons` uses `BUSINESS_CLOSED`, `BUSINESS_REOPENED`, `PHONE_REMOVED`, `WEBSITE_REMOVED`, `RATING_DROP`, `HOURS_CHANGED`, `HOURS_REMOVED`, `ADDRESS_CHANGED`, `NAME_CHANGED`, `BUSINESS_UNCLAIMED`.

#### Business summary row (`rowType: "business_monitor_summary"`)

A real summary row from the first run of the Sydney landmarks input:

```json
{
    "rowType": "business_monitor_summary",
    "monitorKey": "readme-sydney-landmarks",
    "input": "ChIJq6qqWiSsEmsRJuIpepyEua4",
    "placeId": "ChIJq6qqWiSsEmsRJuIpepyEua4",
    "cid": "12590240040890262054",
    "featureId": "0x6b12ac245aaaaaab:0xaeb9849c7a29e226",
    "name": "Taronga Zoo Sydney",
    "googleMapsUrl": "https://www.google.com/maps/place/?q=place_id:ChIJq6qqWiSsEmsRJuIpepyEua4&hl=en",
    "address": "Bradleys Head Rd, Mosman NSW 2088, Australia",
    "status": "BASELINE_CREATED",
    "snapshotQuality": "HIGH",
    "fieldCoverage": {
        "name": true,
        "address": true,
        "coordinates": true,
        "categories": true,
        "phone": true,
        "website": true,
        "openingHours": true,
        "rating": true,
        "reviewCount": true,
        "status": true
    },
    "changesDetected": 0,
    "highestSeverity": null,
    "changeTypes": [],
    "alertTriggered": false,
    "alertReasons": [],
    "firstTrackedAt": "2026-10-24T01:21:50.095Z",
    "previousObservedAt": null,
    "checkedAt": "2026-10-24T01:21:50.095Z",
    "snapshotCount": 1,
    "periodComparisons": null,
    "carriedForwardFields": [],
    "pendingRemovals": [],
    "provider": "direct",
    "current": {
        "openStatus": "OPEN",
        "primaryCategory": "Zoo",
        "categories": ["Zoo", "Animal protection organization", "Tourist attraction"],
        "address": "Bradleys Head Rd, Mosman NSW 2088, Australia",
        "phone": "+61 2 9969 2777",
        "website": "https://taronga.org.au/sydney-zoo",
        "rating": 4.5,
        "reviewCount": 36546,
        "priceLevel": null,
        "isClaimed": true,
        "openingHours": {
            "Monday": ["09:30-17:00"],
            "Tuesday": ["09:30-17:00"],
            "Wednesday": ["09:30-17:00"],
            "Thursday": ["09:30-17:00"],
            "Friday": ["09:30-17:00"],
            "Saturday": ["09:30-17:00"],
            "Sunday": ["09:30-17:00"]
        },
        "latitude": -33.8435473,
        "longitude": 151.2413418,
        "typicalTimeSpent": null
    },
    "error": null
}
```

Fields: `status` (`BASELINE_CREATED`, `NO_CHANGES`, `SUCCESS` or a failure status), `snapshotQuality`, `fieldCoverage`, `changesDetected`, `highestSeverity`, `changeTypes`, `alertTriggered`, `firstTrackedAt`, `previousObservedAt`, `snapshotCount`, `periodComparisons` (rating delta, review delta and new reviews per day vs. 7/30/… days ago), `pendingRemovals`, `carriedForwardFields` and the `current` values (status, category, address, phone, website, rating, review count, hours, …).

Failure statuses (always free, always get a summary row): `PLACE_NOT_FOUND`, `PLACE_ID_CHANGED`, `PARTIAL_PLACE`, `RATE_LIMITED`, `BLOCKED`, `PROVIDER_ERROR`, `STATE_READ_ERROR`, `STATE_WRITE_ERROR`. Retryable places are saved to the run's key-value store as `RETRY_INPUT`, ready to run again.

### ❓ FAQ

#### What happens on the first run?

Each place's current state is stored as its **baseline** (`status: "BASELINE_CREATED"`). No change rows are produced unless you turn on `emitBaseline`, which adds one `BASELINE_CREATED` row with the stored snapshot. A place that is new to a monitor is never reported as a "new business" — use **Google Maps New Business Finder** to discover newly opened places.

#### Which fields are monitored?

Open status, name, categories, address, map pin, phone, website, opening hours (plus special/holiday hours), rating, review count, price level, claim status, attributes, and — with `includePopularTimes` — Popular Times and typical visit duration. The main photo is opt-in (`PHOTO` in `trackFields`). Price level is only compared when Google shows it, which is rare for signed-out visitors.

#### How are closures and reopenings detected?

From Google's own listing status, normalized to `OPEN`, `TEMPORARILY_CLOSED`, `PERMANENTLY_CLOSED` or `UNKNOWN`. Moving from open to closed emits `BUSINESS_TEMPORARILY_CLOSED` or `BUSINESS_PERMANENTLY_CLOSED`; closed back to open emits `BUSINESS_REOPENED`. A reversal of a *permanent* closure is reported with `confidence: "MEDIUM"`, because Google occasionally corrects these. An `UNKNOWN` read never produces a status event.

#### How are opening-hours changes compared?

Hours are normalized to a structured week (`"Monday": ["07:00-15:30"]`, `[]` = closed, `"00:00-24:00"` = open 24 hours), so "9 am" vs "09:00" is never a change. One `OPENING_HOURS_CHANGED` row per business lists every changed day in `change.details`. Holiday / special hours are kept separately and never overwrite the weekly baseline. Hours are only compared when all seven days were read.

#### What counts as a rating change?

`|current − previous| ≥ minimumRatingChange` (default 0.1 stars). Drops are MEDIUM and raise a `RATING_DROP` alert; increases are LOW.

#### Does it monitor review bodies?

No — it tracks the rating and review count (with new reviews per day and velocity spikes). For the reviews themselves, chain **Google Maps Reviews Scraper**, e.g. when `REVIEW_COUNT_CHANGED` fires. The Actor never infers review fraud from a count drop.

#### How are missing fields handled?

**A field missing from one read is never treated as removed.** Google sometimes returns partial details. When a field disappears:

- in a read that isn't complete (`snapshotQuality` below `HIGH`), the previous value is kept (`carriedForwardFields`);
- in a complete read, it becomes a **pending removal** (`pendingRemovals`) and is reported as `PHONE_REMOVED` / `WEBSITE_REMOVED` / `OPENING_HOURS_REMOVED` only if it is still missing on the **next** complete check.

A failed fetch never overwrites stored state.

#### What is snapshot quality?

Each read is scored `HIGH` (identity, name, address, coordinates, categories and a consistent rating/review count), `PARTIAL` (enough to compare the basics; events get at most `MEDIUM` confidence and nothing is removed) or `LOW` (not used: the place gets `PARTIAL_PLACE`, its state is kept, and it isn't charged). `fieldCoverage` shows which fields were present.

#### Does it track Popular Times?

Yes, with `includePopularTimes: true`. Hour-by-hour values are compared per slot against `minimumPopularTimesChange` and summarized (busiest day/hour change, average activity delta). Popular Times are Google's **relative** activity index — not visitor counts. This option loads one Google Search page per place in a browser, so it is slower and billed as a separate add-on event.

#### Can I monitor Place IDs, CIDs or URLs directly?

Yes — and it's the fastest way. Place IDs, CIDs, feature IDs and Maps URLs are checked without a browser. Business names (`"Name, City"`) are looked up on Google Maps first; if a name matches several places, that entry returns `PLACE_NOT_FOUND` with the candidates rather than guessing.

#### Can I use an existing Apify dataset?

Yes — pick it in **Existing Google Maps dataset**. Each row's `placeId`, `cid`, `featureId` or Google Maps URL is used (also inside nested `place` / `business` objects). You can combine it with `places`.

#### How is history stored?

- **State** lives in a named key-value store (`stateStoreName`, default `google-maps-business-monitor-state`) in your account, one record per monitor key + place, so it survives between runs.
- **Snapshots for comparisons** are kept inside that record, thinned to one a day for a month, one a week to six months, then one a month, up to `maxHistoryDays`.
- **Every check** is appended to the dataset `<stateStoreName>-history` (with `saveHistory` on).
- Different `monitorKey` values never share state. Use the same key on every scheduled run.

#### Does it require a Google account or API key?

No. It uses publicly visible Google Maps data — no login, no Google Places API key.

#### How often should it be scheduled?

- **Daily** — competitors, restaurants, retail chains, local SEO clients.
- **Weekly** — large lead databases and slower-moving businesses.
- **Every few hours** — only for exceptional operational needs.

Changes are detected between discrete checks, so `detectedAt` is when the Actor noticed a change — not when the business made it. Two runs of the same monitor key never overlap: a second run started while the first is still running stops immediately without checking or charging anything.

#### Can it trigger webhooks?

Yes. Add an Apify webhook on *Run succeeded* and read the dataset filtered by `alertTriggered`, or use the Apify integrations for Slack, email, Google Sheets, Make, Zapier or n8n. Example dataset URL for alert rows only:

```
https://api.apify.com/v2/datasets/<datasetId>/items?view=overview&clean=true
```

then keep rows where `alertTriggered` is `true`.

#### Can I call it through the API or MCP?

Yes — like any Apify Actor:

```bash
curl -X POST "https://api.apify.com/v2/acts/coregent~google-maps-business-change-monitor/runs?token=<TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"places": ["ChIJ3S-JXmauEmsRUcIaWtf4MzE"], "monitorKey": "competitors"}'
```

It is also available to AI agents through the Apify MCP server (`apify/actors-mcp-server`) as `coregent/google-maps-business-change-monitor`.

### 💰 How does pricing work?

Pay per event — you pay for successful place checks, not for the number of changes:

- **Place check** — once per successfully checked business (baseline created, no changes, or changes found), however many change rows it produces. Change detection is included.
- **Popular Times check** (add-on) — only when `includePopularTimes` is on, once per business whose Popular Times lookup succeeded.

Failed, partial, duplicate and unresolved places and all summary rows are free. Monitoring costs the same whether 1 or 100 things changed, so the bill is predictable. This README deliberately doesn't quote figures — see the **Pricing** tab for current rates. The Actor respects your **Maximum cost per run**: once reached, it stops checking and lists the remaining places in `RETRY_INPUT`.

#### 🚦 Proxy policy

Place checks by URL or ID go straight to Google Maps and need no proxy. The proxy setting applies to the browser steps — business-name lookups and Popular Times. Use **Apify Datacenter** proxy (default) or **no proxy**.

**Apify Residential proxy is not supported.** The Actor fails at startup if `apifyProxyGroups` includes `RESIDENTIAL`. Reason: in pay-per-event Actors, residential bandwidth is billed to the developer, not the run user, so a bandwidth-heavy run could exceed the per-check revenue.

If you genuinely need residential routing, supply your own provider via the proxy editor's **Custom proxy URLs** field — that traffic goes through your provider, not Apify:

```
http://user:pass@proxy.iproyal.com:12321
http://user:pass@proxy.brightdata.com:22225
http://user:pass@proxy.oxylabs.io:7777
```

### 🔗 Related Coregent Google Maps Actors

- **Google Maps Reviews Scraper** — full review text, reviewers and owner replies.
- **Google Maps Popular Times Scraper** — deep Popular Times extraction.
- **Google Maps Photos Scraper** — place photos.
- **Google Maps New Business Finder** — discover newly opened businesses.
- **Google Maps Email & Social Enricher** — website emails and social links.

### ⚖️ Good to know

- Unofficial product; not affiliated with or endorsed by Google.
- Business data can change between observations; changes are detected from discrete snapshots, and history starts at the first baseline.
- Google may temporarily return partial details — missing data is never automatically treated as a removal.
- Popular Times are relative activity indicators, not visitor counts.
- Severity, confidence, signals and alerts are Coregent-derived classifications.
- The Actor does not claim official Google Business Profile audit history, the exact time a business changed a field, actual foot traffic, or confirmed ownership changes, sales or reasons for closure.

# Actor input Schema

## `places` (type: `array`):

One business per line: a Google Maps URL or share link, a Place ID (ChIJ…), a CID (cid:123… or digits), a feature ID (0x…:0x…) or a business name with its location ("Single O, Surry Hills NSW"). The sample place is ignored automatically when you also give a dataset.

## `datasetId` (type: `string`):

A dataset from a previous Google Maps scraper run. Each row's placeId, cid, featureId or Google Maps URL is monitored.

## `maxPlaces` (type: `integer`):

Upper bound on places checked in one run (places + dataset rows, after deduplication).

## `monitorKey` (type: `string`):

Name of this monitor, e.g. competitor-stores or franchise-locations. State and history are kept per monitor key, so the same business can be tracked in independent projects. Use the same key on every scheduled run.

## `mode` (type: `string`):

MONITOR: compare each place with its last stored snapshot and return only new changes (first run creates the baseline). BASELINE: store the current state for every place without returning changes. COMPARE: compare the current state with the snapshot from each comparison period (e.g. 30 days ago).

## `comparisonPeriods` (type: `array`):

COMPARE mode: the periods to compare against (one set of change events each). MONITOR mode: added to each business summary as a period comparison. Needs stored history (Save history on).

## `trackFields` (type: `array`):

Which fields produce change events. Default: every core field except the main photo. Popular Times and visit duration also need "Include Popular Times".

## `includePopularTimes` (type: `boolean`):

Also check the weekly Popular Times histogram and typical visit duration. Slower (one Google Search page per place in a browser) and charged as a separate add-on event.

## `includeAttributes` (type: `boolean`):

Track listing attributes Google shows publicly, such as wheelchair access. Coverage varies by category and region.

## `includeProfileSummary` (type: `boolean`):

Add one free summary row per checked business (status, snapshot quality, changes, period comparisons, current values). Failed places always get a summary row.

## `emitBaseline` (type: `boolean`):

On a place's first check, add one BASELINE_CREATED row with the stored snapshot. Off by default: a first run never produces "field added" change events.

## `minimumRatingChange` (type: `number`):

Smallest rating movement (stars) that counts as a change, e.g. 0.1. Avoids rounding noise.

## `minimumReviewCountChange` (type: `integer`):

Smallest change in the number of reviews that produces an event.

## `minimumPopularTimesChange` (type: `integer`):

Percentage points an hourly Popular Times value must move to count as a change.

## `minimumLocationShiftMeters` (type: `integer`):

Smaller coordinate moves are treated as Google pin corrections and ignored.

## `saveHistory` (type: `boolean`):

Keep past snapshots for period comparisons and append every check to the history dataset (<state store>-history). Off: only the latest snapshot is kept.

## `maxHistoryDays` (type: `integer`):

How far back snapshots are kept for comparisons (thinned to one a day, then one a week, then one a month).

## `stateStoreName` (type: `string`):

Named key-value store that holds the monitor state between runs (letters, digits, hyphens). Change it only to keep a completely separate set of monitors.

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

Google Maps interface language for names, categories and hours (e.g. en, de). Keep it the same across runs — changing it starts a new baseline.

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

Used for the browser steps only (business-name lookups and Popular Times). Place checks by URL / ID go straight to Google Maps. Apify Residential is not supported — use Datacenter or your own proxy URLs.

## Actor input object example

```json
{
  "places": [
    "ChIJ3S-JXmauEmsRUcIaWtf4MzE"
  ],
  "maxPlaces": 1000,
  "monitorKey": "default",
  "mode": "MONITOR",
  "comparisonPeriods": [
    "PREVIOUS",
    "7_DAYS",
    "30_DAYS"
  ],
  "trackFields": [
    "STATUS",
    "NAME",
    "CATEGORIES",
    "ADDRESS",
    "COORDINATES",
    "PHONE",
    "WEBSITE",
    "OPENING_HOURS",
    "RATING",
    "REVIEW_COUNT",
    "PRICE_LEVEL",
    "CLAIM_STATUS",
    "ATTRIBUTES",
    "POPULAR_TIMES",
    "TYPICAL_VISIT_DURATION"
  ],
  "includePopularTimes": false,
  "includeAttributes": true,
  "includeProfileSummary": true,
  "emitBaseline": false,
  "minimumRatingChange": 0.1,
  "minimumReviewCountChange": 1,
  "minimumPopularTimesChange": 10,
  "minimumLocationShiftMeters": 50,
  "saveHistory": true,
  "maxHistoryDays": 365,
  "stateStoreName": "google-maps-business-monitor-state",
  "language": "en",
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `overview` (type: `string`):

One row per detected business change event with severity, confidence, previous and current values.

## `businesses` (type: `string`):

Free per-business summary rows: status, snapshot quality, changes found and current values.

# 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 = {
    "places": [
        "ChIJ3S-JXmauEmsRUcIaWtf4MzE"
    ],
    "includeAttributes": true,
    "includeProfileSummary": true,
    "saveHistory": true,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("coregent/google-maps-business-change-monitor").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 = {
    "places": ["ChIJ3S-JXmauEmsRUcIaWtf4MzE"],
    "includeAttributes": True,
    "includeProfileSummary": True,
    "saveHistory": True,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("coregent/google-maps-business-change-monitor").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 '{
  "places": [
    "ChIJ3S-JXmauEmsRUcIaWtf4MzE"
  ],
  "includeAttributes": true,
  "includeProfileSummary": true,
  "saveHistory": true,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call coregent/google-maps-business-change-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,coregent/google-maps-business-change-monitor"
        }
    }
}
```

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/v0AwHCocX6rzxtJxj/builds/xB3d9SIEduepFFgf0/openapi.json
