# Google Maps Business Scraper & Monitor: Leads by Category (`nswk_api/google-maps-business-monitor`) Actor

List businesses from Google Maps by category and location: name, category, rating, reviews count, address, website, opening status and coordinates. Changes mode returns only new places, closures and rating moves. Phone optional. No login, no reviews.

- **URL**: https://apify.com/nswk_api/google-maps-business-monitor.md
- **Developed by:** [Bryan Masias](https://apify.com/nswk_api) (community)
- **Categories:** Lead generation, AI
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

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 Scraper & Monitor: Leads by Category

List the businesses Google Maps shows for any category in any city: **name, category, rating,
reviews count, address, website, opening status, coordinates and place ID**. Or schedule it in
**changes** mode and get only what moved since the last run: **new businesses, closures, new
websites and rating changes**.

Business data only. No login, no reviews, no photos, no reviewer names. The phone number is optional and off by default.

### What it does

- Searches Google Maps for every search term in every location (e.g. "dentist" × "Austin, Texas").
- Scrolls the result list up to your limit (Google stops at about 120 places per search).
- Reads each business from the list, so one search page gives up to 120 places.
- Optionally opens each place page (`includeDetails`) for the full address with postal code, website, phone, plus code and weekly opening hours.
- Removes duplicates found by more than one search.
- Applies your filters **before** charging: rating, reviews count, with or without website, category.
- In changes mode, compares with the last run and returns only the differences.

### Use cases

- **Lead lists:** every plumber, dentist or gym in a city, with website and rating. Use `website: "without"` to find businesses that still have no site.
- **Market research:** count competitors by category and neighborhood, compare ratings.
- **Monitoring:** a weekly run tells you which businesses opened, closed or changed their website in your area.
- **AI agents:** two input fields, a flat output, and you only pay for places returned.

### Input

```json
{
  "searchTerms": ["dentist", "orthodontist"],
  "locations": ["Austin, Texas", "Round Rock, Texas"],
  "maxPlacesPerSearch": 60,
  "minRating": 4,
  "mode": "all"
}
```

| Field | Default | What it does |
| --- | --- | --- |
| `searchTerms` | — | What to look for. Each term is searched in each location. |
| `locations` | — | Where to look. Leave empty if your terms already say where. |
| `queries` | — | Full queries as typed in Google Maps, added to the others. |
| `maxPlacesPerSearch` | `60` | Up to 120. For more places, split a city into neighborhoods. |
| `mode` | `all` | `all` returns every place; `changes` returns only new, updated or missing places. |
| `monitorId` | from searches + filters | Name of the saved state for changes mode. |
| `includePhone` | `false` | Add the business phone shown on the listing. |
| `includeDetails` | `false` | Open each place page for full address, website, phone, plus code and opening hours. Turned on by the `website` filter. |
| `minRating` | — | Keep places rated at least this much. |
| `minReviews` | — | Keep places with at least this many reviews. |
| `website` | `any` | `with` or `without` a website. Reads each place page, so it turns on details. |
| `categories` | — | Keep places whose Google category contains any of these. |
| `excludeClosed` | `true` | Skip permanently closed places. |
| `maxConcurrency` | `2` | Search pages loaded at once (1 to 3). |

Up to 200 searches per run.

### Output

One item per business:

```json
{
  "placeId": "ChIJf9IQjhDTFJYRg8x0vx6FLNg",
  "cid": "15576971578275318915",
  "name": "Urgencia Dental Temuco- Dentista Temuco",
  "category": "Dentist",
  "rating": 4.9,
  "reviewsCount": 288,
  "priceLevel": null,
  "address": "España 460, Ofi 605",
  "description": null,
  "openingStatus": "Open · Closes 9 PM",
  "businessStatus": "OPERATIONAL",
  "website": "https://brunovalderramacea.wixsite.com/urgenciadentaltemuco",
  "latitude": -38.733945,
  "longitude": -72.6123218,
  "url": "https://www.google.com/maps/place/?q=place_id:ChIJf9IQjhDTFJYRg8x0vx6FLNg",
  "searchQuery": "dentist in Temuco, Chile",
  "rank": 1,
  "scrapedAt": "2026-10-26T22:00:00.000Z"
}
```

With `includePhone: true` the item also has `phone`. With `includeDetails: true` it also has
`fullAddress`, `plusCode` and `openingHours` (one entry per day, e.g. `{"day": "Monday", "hours": "8 AM to 5 PM"}`),
and `website` comes from the place page. Without details, `website` is only filled when the list shows a
Website button, which Google does for some categories only. `detailsLoaded` says which one you got. `businessStatus` is `OPERATIONAL`,
`TEMPORARILY_CLOSED` or `PERMANENTLY_CLOSED`. `address` is the street address as shown in the
list. `openingStatus` is the text Google shows at the time of the run.

In changes mode each item also has `changeType`, `changes` and `monitorId`:

```json
{
  "changeType": "UPDATED",
  "changes": [{ "field": "businessStatus", "previous": "OPERATIONAL", "current": "PERMANENTLY_CLOSED" }],
  "name": "Clínica Cataluña - Dentista en Temuco",
  "monitorId": "austin-dentists"
}
```

- `NEW`: a place not seen before. The first run returns every place as `NEW`.
- `UPDATED`: name, category, address, price level, status, website or phone changed, or the rating moved by 0.1 or more (small moves add up).
- `MISSING`: a known place was not in the list for two complete runs in a row. Lists shift a little between runs, so this is a hint, not proof of closure.

For monitoring, set `maxPlacesPerSearch` to 120: with a short limit, places near the cut move in and out of the list and show up as `NEW` or `MISSING`.

The run summary (key-value store record `OUTPUT`) has the searches done, places found and
filtered, changes by type, and any search that failed.

### Example run

The prefilled input runs in about a minute and costs a few cents at most. It returns up to 20 places.

Input:

```json
{
  "searchTerms": ["dentist"],
  "locations": ["Austin, Texas"],
  "maxPlacesPerSearch": 20
}
```

First result (long text shortened):

```json
{
  "placeId": "ChIJb5gBC-u0RIYRHkntaUcdJGs",
  "name": "ATX Family Dental",
  "category": "Dentist",
  "rating": 4.9,
  "reviewsCount": 916,
  "address": "1700 S 1st St",
  "businessStatus": "OPERATIONAL",
  "latitude": 30.2482229,
  "longitude": -97.7559705,
  "searchQuery": "dentist in Austin, Texas",
  "rank": 1
}
```

### Pricing

Pay per event:

| Event | Price | When |
| --- | --- | --- |
| Place result | $0.0015 ($1.50 per 1,000) | Each place returned. In changes mode, each change returned. |
| Place page read | $0.002 | Only with details on (or a website filter): each place page read for a place returned, or, in changes mode, for each tracked place checked. |

Places removed by your filters, failed searches and pages Google did not serve are free.
Without details, 1,000 places cost $1.50; with details, $3.50.

### How it works and limits

- It opens Google Maps in a regular headless Chromium, through Apify datacenter proxies, in English (`hl=en`) so the fields read the same everywhere. Names and addresses stay as Google shows them.
- By default it loads only the search pages, which keeps it fast and cheap. With details on it also loads each place page, about 4 to 8 seconds per place. Reviews and photos are never collected.
- It does not retry a page that Google answered with a challenge, and it stops the run if that happens on more than 5% of the pages. You are not charged for those searches.
- Google decides the order and the number of places in each list. Two runs of the same search can differ slightly.

### Responsible use

This Actor returns business listings that Google Maps shows publicly. It does not collect reviews,
reviewer names or photos. Some small businesses use a personal mobile number as their business
phone; that is why phones are off by default. If you turn them on, use them only in line with the
privacy, anti-spam and telemarketing laws that apply to you (for example GDPR, CAN-SPAM or TCPA).
You are responsible for how you use the data and for complying with Google's terms.

# Actor input Schema

## `searchTerms` (type: `array`):

What to look for, e.g. "dentist", "hardware store", "vegan restaurant". Each term is searched in each location.

## `locations` (type: `array`):

Where to look, e.g. "Austin, Texas" or "Temuco, Chile". Leave empty if your search terms already include the place.

## `queries` (type: `array`):

Optional. Queries exactly as you would type them in Google Maps, e.g. "coffee shops in Lima". Added to the term × location searches.

## `maxPlacesPerSearch` (type: `integer`):

Google's list ends at about 120 places per search. Split big cities into neighborhoods to get more.

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

"all" returns every place. "changes" returns only places that are new, updated (closed, new website, rating move...) or missing since the last run with the same monitor ID. Schedule it weekly.

## `monitorId` (type: `string`):

Name of the saved state for changes mode. By default it is derived from your searches and filters.

## `includePhone` (type: `boolean`):

Add the phone number shown on the business listing. Off by default. Some small businesses list a personal mobile number: only use phones in line with the privacy and marketing laws that apply to you.

## `includeDetails` (type: `boolean`):

Also read each place's own page: full address with postal code, website, phone, plus code and weekly opening hours. The list alone shows the website only for some categories. Charged per place page read. Turned on automatically when you filter by website.

## `minRating` (type: `number`):

Keep places rated at least this much (0 to 5).

## `minReviews` (type: `integer`):

Keep places with at least this many reviews.

## `website` (type: `string`):

"without" finds businesses with no website, a classic lead list for web agencies. This filter reads each place page, so it turns on details.

## `categories` (type: `array`):

Keep places whose Google category contains any of these, e.g. "dental clinic". Case-insensitive.

## `excludeClosed` (type: `boolean`):

Drop places marked permanently closed. In changes mode a known place that closes is still reported.

## `maxConcurrency` (type: `integer`):

Search pages loaded at once (1 to 3).

## Actor input object example

```json
{
  "searchTerms": [
    "dentist"
  ],
  "locations": [
    "Austin, Texas"
  ],
  "maxPlacesPerSearch": 20,
  "mode": "all",
  "includePhone": false,
  "includeDetails": false,
  "website": "any",
  "excludeClosed": true,
  "maxConcurrency": 2
}
```

# Actor output Schema

## `places` (type: `string`):

One item per business, or one item per change in changes mode.

## `summary` (type: `string`):

Searches done, places found and filtered, changes by type, and searches that failed or were blocked.

# 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 = {
    "searchTerms": [
        "dentist"
    ],
    "locations": [
        "Austin, Texas"
    ],
    "maxPlacesPerSearch": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("nswk_api/google-maps-business-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 = {
    "searchTerms": ["dentist"],
    "locations": ["Austin, Texas"],
    "maxPlacesPerSearch": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("nswk_api/google-maps-business-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 '{
  "searchTerms": [
    "dentist"
  ],
  "locations": [
    "Austin, Texas"
  ],
  "maxPlacesPerSearch": 20
}' |
apify call nswk_api/google-maps-business-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,nswk_api/google-maps-business-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/P75UxLjIj2na7YhSp/builds/31RvFoi5M6i0juVE7/openapi.json
