# Google Maps New Business & Competitor Openings Monitor (`mibedk/google-maps-new-openings-monitor`) Actor

Runs a Google Maps place search for an area and business category and emits only the listings that are new since the previous run, so you get an alert-style feed of new openings instead of a full re-download every time.

- **URL**: https://apify.com/mibedk/google-maps-new-openings-monitor.md
- **Developed by:** [Mikkel Bech-Hansen](https://apify.com/mibedk) (community)
- **Categories:** E-commerce, Integrations, Other
- **Stats:** 1 total users, 0 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $10.00 / 1,000 listings

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?

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 New Business & Competitor Openings Monitor

Runs a Google Maps search for an area and business category (e.g. "coffee shop in
Austin, TX") and reports only the listings that are **new since the last time you ran
it**. Instead of re-downloading every business in the area on every run, you get an
alert-style feed of what just opened — useful for site-selection scouting, franchise
expansion research, or watching a competitor's category for new entrants.

The first run for a query has nothing to compare against, so it reports every listing
found as a baseline (each record is flagged `isFirstRunBaseline: true`). From the second
run onward, only genuinely new listings are reported.

### How it works

Google Maps' search results are loaded by the same public, no-login search surface that
`maps.google.com` itself uses — no API key, no Places API billing. This actor drives a
real browser (Playwright) to the search results feed, scrolls it to collect listings, and
for every listing that's new since the previous run, opens that listing's own Maps page to
also pull its phone number, website and review count. That second step only runs for
listings that turned out to be new, so a quiet run (nothing new opened) stays cheap.

Each business is tracked by the place ID Google Maps embeds in its own listing links
(the same `ChIJ...` identifier used across Google's products), stored in a named
key-value store between runs so scheduled runs keep comparing against the same history.

### Input

| Field | Type | Default | Notes |
|---|---|---|---|
| `searchQueries` | array of strings | `["coffee shop in Austin, TX"]` | One entry per area+category. Write it exactly as you'd type it into the Google Maps search box. |
| `maxResultsPerQuery` | integer | `60` | How many listings to scan per query (1–120). Google Maps rarely returns more than ~120 for one search. Does not limit how many *new* listings can be reported. |
| `enrichNewListings` | boolean | `true` | Fetch phone/website/review count for each newly detected listing. Adds one page load per new listing, not per scanned listing. |
| `stateStoreName` | string | `google-maps-new-openings-monitor-state` | Key-value store used to remember what was already seen. Leave at default so scheduled runs keep comparing against the same history; change only to run two independent monitors from the same account. |
| `proxyConfiguration` | object | no proxy | Off by default so a first run is free to try. Google will start showing consent/CAPTCHA pages if you run many queries from the same IP in a short time — enable Apify Proxy (residential recommended) once you monitor more than a handful of queries. |
| `maxConcurrency` | integer | `2` | Search queries processed in parallel. Kept low by default — each one drives a real browser page, and going higher raises the chance of a CAPTCHA. |
| `requestTimeoutSecs` | integer | `90` | Timeout for a single Maps page load (search or place detail) before it's retried. |
| `maxRequestRetries` | integer | `2` | Retries per page after a failed load. |

### Output

One dataset record per newly detected listing:

```json
{
  "placeId": "ChIJG-gJw2vKRIYROWi2uwOp8QE",
  "name": "Epoch Coffee",
  "category": "Coffee shop",
  "address": "221 W N Loop Blvd, Austin, TX 78751, United States",
  "phone": "+1 512-454-3762",
  "website": "http://www.epochcoffee.com/",
  "rating": 4.5,
  "reviewCount": 2511,
  "latitude": 30.3186037,
  "longitude": -97.7245402,
  "mapsUrl": "https://www.google.com/maps/place/Epoch+Coffee/...",
  "searchQuery": "coffee shop in Austin, TX",
  "isFirstRunBaseline": true,
  "detectedAt": "2026-08-17T23:31:41.159Z"
}
```

`phone`, `website` and `reviewCount` are only populated when `enrichNewListings` is on;
otherwise they're `null`. `address` and `rating` come from the search results feed either
way. `isFirstRunBaseline` is `true` for every listing reported the first time a given
query is run (see above) and `false` for genuinely new openings detected afterward —
filter on this field if you only want real alerts.

### Cost guidance

With the suggested pay-per-event pricing (see `MONETIZATION.md`): `area-checked` ($0.002)
fires once per search query per run regardless of results, `new-listing-detected` ($0.01)
fires once per newly found listing. A typical scheduled run of 5 tracked areas that turns
up 3 new listings costs about 5 × $0.002 + 3 × $0.01 ≈ **$0.04**. The first run on a fresh
query is more expensive since every listing counts as a "new" baseline record — scanning
60 listings for one query on day one costs about $0.002 + 60 × $0.01 = **$0.60**.

### Known limitations

- **This is not the official Places API.** It reads the same public search results page
  `maps.google.com` itself renders. It's free to run (no Places API billing) but is not
  as stable a contract as a paid API — see Legal below.
- **Google Maps compact search cards don't show review counts.** Review count is only
  available by opening each listing's own page, which is why it's only fetched for newly
  detected listings, not for every scanned listing.
- **Category and street address are parsed from the results feed's visible text**, which
  is a compact, sometimes inconsistent summary (e.g. street address only, no city/zip).
  Turning on `enrichNewListings` gets you the full, structured address instead.
- **No login means no personalization**, which is a plus for consistent results but means
  ranking/order can shift slightly between runs even with no real change on the ground —
  this can occasionally surface a listing as "new" that was just previously outside your
  `maxResultsPerQuery` cutoff, not a genuine new opening.
- **Sponsored/ad listings are filtered out** — they're existing businesses paying for
  placement, not new openings, so they'd otherwise create false "new listing" alerts.
- **Consent walls and CAPTCHAs.** Without a proxy, Google may start showing a consent
  interstitial or CAPTCHA after repeated automated requests from the same IP in a short
  window. If a query's search fails outright, the run logs it and continues with the
  other queries; if every query fails, the run fails rather than reporting a false empty
  success. Add a proxy (see Input) if this happens regularly.
- **State lives in a key-value store, not the dataset.** If you delete or rename the
  `stateStoreName` store, the next run starts over and reports every listing as a new
  baseline again.

### Legal

This Actor is an independent, third-party tool. It is not affiliated with, endorsed by,
or sponsored by Google or Google Maps.

It retrieves data by automating Google Maps' public, no-login search interface rather
than through Google's official, paid Places API. Google's Terms of Service restrict
automated access to Google Maps outside of that API, regardless of whether the
underlying data (business names, addresses, ratings) is publicly visible. Using this
Actor may not comply with those terms. You are responsible for reviewing Google's Terms
of Service and applicable law before using this Actor, and for how you use the data it
returns — including any restrictions on using Maps data to build a competing
mapping/business-directory product, which Google's terms specifically prohibit.

# Actor input Schema

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

One entry per area+category combination you want monitored, written exactly as you would type it into the Google Maps search box (e.g. "coffee shop in Austin, TX" or "hair salon near 10001"). Each query is tracked separately, so you can watch several categories or cities in one run.

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

How many listings to pull from the Google Maps results feed for each search query before comparing against the previous run. Google Maps rarely returns more than ~120 results for one search. A lower number keeps runs fast and cheap; it does not limit how many *new* listings can be reported.

## `enrichNewListings` (type: `boolean`):

When a listing is newly detected, open its Google Maps place page to also capture phone number, website and total review count. This adds one extra page load per new listing only (not per scanned listing), so it stays cheap on runs where nothing new opened.

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

Name of the key-value store used to remember which listings were already seen, so future runs can detect what's new. Leave this at the default so scheduled runs of this Actor keep comparing against the same history. Only change it if you are running two independent monitors from the same Apify account and want them to keep separate history.

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

Optional. Google Maps works without a proxy for light, occasional use, but will start showing consent/CAPTCHA pages if you run many queries from the same IP in a short time. Enable Apify Proxy (residential recommended) if you monitor more than a handful of queries or run on a tight schedule.

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

Maximum number of search queries processed in parallel. Kept low by default because each query drives a real browser page; raising it speeds up multi-query runs but increases the chance Google shows a CAPTCHA.

## `requestTimeoutSecs` (type: `integer`):

How long to wait for a single Google Maps page (search or place detail) to load before it counts as a failed attempt and is retried.

## `maxRequestRetries` (type: `integer`):

How many times to retry a search or place-detail page after a failed load (timeout, block page, network error) before giving up on it.

## Actor input object example

```json
{
  "searchQueries": [
    "coffee shop in Austin, TX"
  ],
  "maxResultsPerQuery": 60,
  "enrichNewListings": true,
  "stateStoreName": "google-maps-new-openings-monitor-state",
  "proxyConfiguration": {
    "useApifyProxy": false
  },
  "maxConcurrency": 2,
  "requestTimeoutSecs": 90,
  "maxRequestRetries": 2
}
```

# Actor output Schema

## `newListings` (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 = {
    "searchQueries": [
        "coffee shop in Austin, TX"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("mibedk/google-maps-new-openings-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": ["coffee shop in Austin, TX"] }

# Run the Actor and wait for it to finish
run = client.actor("mibedk/google-maps-new-openings-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": [
    "coffee shop in Austin, TX"
  ]
}' |
apify call mibedk/google-maps-new-openings-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,mibedk/google-maps-new-openings-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/pVAtfOcD5Mh3zcyce/builds/YwJUuVRIFaCeKsCPU/openapi.json
