# Google Maps Business Verification & Status Monitoring (`fanndev/google-maps-business-status-monitor`) Actor

Find neglected Google Business Profiles in any area: unclaimed listings, businesses with no website or phone, thin profiles and closed locations - each scored as a GMB sales opportunity. Built for local SEO agencies prospecting claim and optimisation clients. JSON/CSV/Excel/NDJSON.

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

## Pricing

from $0.50 / 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/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 Verification & Status Monitoring

Find neglected Google Business Profiles in any area — **unclaimed listings**,
businesses with **no website** or no phone, thin profiles, and closed locations —
each scored as a Google Business Profile sales opportunity.

Built for local SEO agencies prospecting claim-and-optimise clients: instead of
checking listings one at a time in Maps, sweep a whole category across a city and
get a ranked call list.

No login, no Google API key, no browser.

***

### What it tells you per business

| Field | Notes |
| --- | --- |
| **`claimStatus`** / `isUnclaimed` | `unclaimed` means nobody has taken the profile — your core pitch |
| **`businessStatus`** | `operational`, `temporarily_closed`, `permanently_closed` (see the limit below) |
| `hasWebsite`, `hasPhone`, `hasOpeningHours` | The profile gaps you would fix |
| `reviewsCount`, `rating` | Thin review counts signal a neglected listing |
| **`opportunityScore`** | 0–100, weighted for GMB sales value |
| **`opportunityIssues`** | The specific problems, e.g. `["unclaimed","no_website","no_phone"]` |
| `name`, `primaryCategory`, `address`, `phone`, `website` | Contact details for the outreach itself |
| `latitude`, `longitude`, `placeId`, `googleMapsUrl` | Location and stable identifiers |

Plus one **`AREA_SUMMARY`** row per run — the market-level pitch: how many
listings in this area are unclaimed, how many lack a website, and how many are
both.

***

### Quick start

```json
{
  "searchQueries": ["warung", "salon", "laundry"],
  "areaName": "jakarta",
  "maxResultsPerQuery": 200,
  "onlyUnclaimed": true,
  "exportFormats": ["csv"]
}
```

Real output from a 20-result sweep of `warung` in Jakarta: **15 unclaimed (75%),
17 without a website, 15 with both**, top prospect scoring 75.

#### It actually discriminates

The same run against other categories, which is how you know the signal is real
rather than a constant:

| Category | Unclaimed |
| --- | --- |
| `warung` (small local eateries) | **15/20** |
| `internet cafe` | 7/20 |
| `salon` | 1/20 |
| `dentist` | **1/20** |

Professional practices are almost always claimed; informal micro-businesses are
almost never. Point the actor at the second group.

#### Building a call list

`onlyUnclaimed`, `onlyWithoutWebsite`, `onlyWithoutPhone` and
`minOpportunityScore` narrow the sweep to businesses worth a call. They combine,
so `onlyUnclaimed + onlyWithoutWebsite` gives you the strongest prospects only —
and legitimately returns nothing for a well-managed category.

`minOpportunityScore: 65` is a good starting threshold.

#### Locations

`areaName` for a built-in city or country, or a centre point with `radiusKm`, or
an exact `boundingBox`. Areas are tiled into multiple search points and then
filtered against your exact area using each business's real coordinates.

***

### How the score works

| Signal | Points |
| --- | --- |
| Unclaimed listing | 40 |
| No website | 25 |
| No phone | 10 |
| No opening hours | 10 |
| Fewer than 5 reviews | 10 |
| Rating below 4.0 | 5 |

A **permanently closed** listing is forced to **0** however neglected it looks —
a dead business is not a prospect and should never sit at the top of your call
list. It still appears with `businessStatus: permanently_closed` so you can see
why.

***

### Limits worth knowing

1. **Closed businesses are hard to discover.** Google appears to exclude
   permanently closed listings from search results — across nine queries in two
   countries, including chains known to be defunct (7-Eleven Indonesia, Toys R
   Us, Blockbuster), not one closed listing came back. Closure is therefore
   reported reliably only for businesses you supply directly via `placeUrls` /
   `placeIds`, or when `verifyWithPlaceDetails` is on. Use this actor to *monitor*
   known businesses for closure, not to *discover* closed ones.
2. **`verifyWithPlaceDetails` costs one extra request per business.** Leave it
   off for large sweeps; turn it on for a shortlist you are about to call.
3. **Claim status reflects what Google exposes publicly.** It is derived from the
   presence of Google's own "Own this business?" action, which is the same signal
   a human sees in Maps.
4. **Category names follow `language`.** Keep `language: "en"` unless you need
   local category names — the closure wording matched is English.

***

### Cost and proxy

Every signal except closure rides in the search response, so a sweep costs **one
request per twenty businesses** — the cheapest actor in this Maps family.

Proxy is **off by default** and that is correct here: the claim signal survives
even the reduced record Google serves datacenter IPs, so the platform's own IP
loses nothing. Residential is not recommended for Google — it draws consent and
CAPTCHA interstitials.

***

### Output shape

Portfolio envelope — `_input`, `_source`, `_scrapedAt`, `recordType` — with
`_error` / `_errorDetail` on failures. `recordType` is `BUSINESS`,
`AREA_SUMMARY` or `ERROR`. Three Console views ship with the actor: **Overview**,
**Sales prospects** and **Status & verification**.

***

### Development

```bash
pip install -r requirements.txt
python test_errors.py     # offline: claim detection, closure logic, scoring, filters
python test_local.py      # live end-to-end run
```

See [CRAWLING\_METHOD.md](CRAWLING_METHOD.md) for the reconnaissance, the
category-by-category validation of the claim signal, and the "every record
contains the word closed" trap.

# Actor input Schema

## `searchQueries` (type: `array`):

Categories to sweep, e.g. 'dentist', 'salon', 'warung'. Each runs across the location below. A location is required with queries.

## `areaName` (type: `string`):

Built-in city or country, e.g. 'jakarta', 'bali', 'london'. For anywhere else use a centre point with a radius, or a bounding box.

## `centerLatitude` (type: `string`):

Latitude of the centre point. Use with centre longitude and radius.

## `centerLongitude` (type: `string`):

Longitude of the centre point.

## `radiusKm` (type: `string`):

Audit businesses within this many kilometres of the centre point.

## `boundingBox` (type: `array`):

Most precise option. Tiled into several search points, then filtered to the box using each business's real coordinates.

## `tileSizeKm` (type: `integer`):

Spacing of search points inside your area. Smaller = more thorough, more requests. Leave empty to choose automatically.

## `maxResultsPerQuery` (type: `integer`):

Stop each query after this many businesses. Google returns 20 per request and every signal here comes from that one request.

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

Optional. Audit named businesses directly instead of sweeping an area - this is also the only way to confirm a closed listing, since Google keeps closed businesses out of search results.

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

Optional. Accepts a Place ID (ChIJ...), a numeric CID, or a raw feature ID (0x...:0x...).

## `onlyUnclaimed` (type: `boolean`):

Keep only businesses that nobody has claimed on Google Business Profile - the core prospect list for a GMB claim service.

## `onlyWithoutWebsite` (type: `boolean`):

Keep only businesses with no website listed - the natural upsell after a profile claim.

## `onlyWithoutPhone` (type: `boolean`):

Keep only businesses with no phone number on their listing.

## `onlyClosed` (type: `boolean`):

Keep only listings marked permanently or temporarily closed. Note Google excludes closed businesses from search results, so this realistically only returns anything for places you supply by URL or ID.

## `excludePermanentlyClosed` (type: `boolean`):

Drop permanently closed listings - they are never a sales prospect.

## `minOpportunityScore` (type: `integer`):

Keep only businesses scoring at or above this. Unclaimed is worth 40, no website 25, no phone 10, no hours 10, few reviews 10, low rating 5. 65+ is a strong pitch.

## `verifyWithPlaceDetails` (type: `boolean`):

Fetch each business's detail page for a second look. Needed to detect closures and to fill in gaps Google trims from search results. Costs one extra request per business, so leave it off for large sweeps.

## `emitAreaSummary` (type: `boolean`):

Add one AREA\_SUMMARY record with market-level totals: how many listings are unclaimed, how many lack a website, and how many are both.

## `exportFormats` (type: `array`):

Also write the results to the key-value store in these formats. The Apify dataset is always produced regardless.

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

Google interface language. Keep as 'en' unless you need local category names - the closure wording this actor matches is English.

## `region` (type: `string`):

Two-letter country code biasing Google's results, e.g. US, ID, GB. Set this to the market you are prospecting.

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

Off by default and that is the right setting here: these endpoints have no WAF and the claim signal survives even Google's reduced record, so the platform's own IP works and costs nothing. RESIDENTIAL is not recommended for Google.

## Actor input object example

```json
{
  "searchQueries": [
    "dentist"
  ],
  "areaName": "jakarta",
  "maxResultsPerQuery": 100,
  "onlyUnclaimed": false,
  "onlyWithoutWebsite": false,
  "onlyWithoutPhone": false,
  "onlyClosed": false,
  "excludePermanentlyClosed": false,
  "verifyWithPlaceDetails": false,
  "emitAreaSummary": true,
  "exportFormats": [],
  "language": "en",
  "region": "US",
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

Every assessed business, the area summary and any error records from this run.

# 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 = {
    "searchQueries": [
        "dentist"
    ],
    "areaName": "jakarta"
};

// Run the Actor and wait for it to finish
const run = await client.actor("fanndev/google-maps-business-status-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 = {
    "searchQueries": ["dentist"],
    "areaName": "jakarta",
}

# Run the Actor and wait for it to finish
run = client.actor("fanndev/google-maps-business-status-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 '{
  "searchQueries": [
    "dentist"
  ],
  "areaName": "jakarta"
}' |
apify call fanndev/google-maps-business-status-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,fanndev/google-maps-business-status-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/8fBO5kuKKQ5ENPYDf/builds/f8Zmfo3efqv2cKSMe/openapi.json
