# Google Maps Business Leads – Phone, Website & Rating (`staticxviper/google-business-lead-generation`) Actor

- **URL**: https://apify.com/staticxviper/google-business-lead-generation.md
- **Developed by:** [CJ Moulton](https://apify.com/staticxviper) (community)
- **Categories:** Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 accepted matches

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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 Match

Low-volume Google Maps lookup for a business name plus a US state. The Actor searches `https://www.google.com/maps/search/`, opens place pages, and scores each listing with the same name and location rules as issue #127. A row is a `match` only when that scorer would accept it. The first Maps card is never used as a fallback.

This is not the Google Places API. That API is a separate paid product and this Actor does not call it.

### What it does

Each query becomes one dataset item:

| `decision` | Meaning |
|------------|---------|
| `match` | Score at or above `minConfidence` (default 80) and no hard reject |
| `low_confidence` | Score at or above `lowConfidenceFloor` (default 60) and below the minimum |
| `no_match` | Hard reject, weak score, or no listing |
| `blocked` | Google showed a block page. Later queries are not run |
| `error` | Input, robots, or unexpected failure |

`hardRejects` can include `brand_token_missing`, `city_mismatch`, `distinctive_token_missing`, `state_mismatch`, `county_mismatch`, `address_mismatch`, `area_code_state_mismatch`, `permanently_closed`, and `website_is_directory`. Legal suffixes such as LLC are stripped before the name comparison. State on the query is required.

A different brand is not returned as the candidate. Retail words (`food`, `market`, `store`, `shop`, `grocery`) do not count as the brand, and every remaining brand token has to match. When the query has a city, a listing in another city is dropped before scoring. A card that already passed that check is scored under the query name, so a Maps title of "Wegmans" can match "Wegmans Food Markets". The row still shows the Maps title. `no_match` leaves `candidate` null.

Known false pairs that stay `no_match`:

- STRIKER CONSTRUCTION SERVICES LLC (NY, Cattaraugus) vs “C. Staker Remodeling”
- B\&B Basecamp LLC (Denver) vs “Base Camp at Golden Gate Canyon”

`includeEmails` defaults to false. When true, emails are read only from the website of a `match`, then validated. The Actor does not guess addresses and does not search Google for them.

### Input

`fixtures/INPUT.json`:

```json
{
  "queries": [
    {
      "name": "Wegmans Food Markets",
      "city": "Rochester",
      "county": "Monroe",
      "state": "NY",
      "maxResults": 3
    }
  ],
  "minConfidence": 80,
  "lowConfidenceFloor": 60,
  "maxSearches": 1,
  "headless": true,
  "includeEmails": false,
  "maxSpendUsd": 5,
  "maxProxyCostUsd": 0.5,
  "allowCaptcha": false,
  "proxyConfiguration": {"useApifyProxy": false}
}
```

A single object (`name`, `state`, optional `city` and `county`) works when `queries` is omitted. `maxResults` defaults to 5 and cannot exceed 20. `maxSearches` defaults to 10 and cannot exceed 50. Search delay defaults to a random 2.0–4.5 seconds and detail delay to 1.2–2.8 seconds, same as the local Maps session. Overrides are optional. `maxSpendUsd` defaults to 5, `maxProxyCostUsd` to 0.50, and `allowCaptcha` to false. The proxy group is used only after a block. The default run timeout is 900 seconds at 2048 MB.

### Output

```json
{
  "query": {
    "name": "Wegmans Food Markets",
    "city": "Rochester",
    "county": "Monroe",
    "state": "NY",
    "maxResults": 3
  },
  "decision": "match",
  "score": 90,
  "breakdown": {"name": 50, "location": 35, "category": 0, "corroboration": 5},
  "hardRejects": [],
  "candidate": {
    "business_name": "Wegmans",
    "phone": "(585) 244-7950",
    "website": "https://www.wegmans.com/stores/east-ave-ny/",
    "address": "1750 East Ave, Rochester, NY 14610",
    "rating": 4.5,
    "user_ratings_total": 4754,
    "category": "Grocery store",
    "mapsUrl": "https://www.google.com/maps/place/Wegmans/",
    "placeId": "ChIJhwPAPIHK1okRJJMPqz59rNE"
  },
  "sourceStatus": "ok",
  "scrapedAt": "2026-10-10T00:00:00+00:00"
}
```

A different business, such as Whole Foods Market in the same city, is not attached to that row. A rejected query looks like `decision: "no_match"`, `candidate: null`, and `hardRejects` of `brand_token_missing` or `city_mismatch`. The dataset Overview shows the decision and score. Detail shows the query, breakdown, hard rejects, and candidate. `sourceStatus` is `ok`, `blocked`, `robots_disallowed`, or `error`. Key-value `SUMMARY` counts pushed, blocked, and error rows.

Candidate fields come from the Maps session: phone, website, address, rating, `user_ratings_total`, category, `mapsUrl`, `placeId`.

### Google terms, robots, and blocks

Google’s terms restrict automated extraction. A robots.txt `Allow` is not permission to automate Maps. This Actor is for occasional research, not bulk lead lists.

`https://www.google.com/robots.txt` is read first and evaluated with longest-match rules (`crm_enrich_robots.py`). Under those rules, `/maps/search/` and `/maps/place/` can be allowed while `Disallow: /maps/` is also present, and `/search` stays disallowed. This Actor never requests `https://www.google.com/search`. Python’s `urllib.robotparser` is not used, because it hides the more specific Maps `Allow` lines.

If Google returns a block page, the Actor retries that query once through Apify Proxy when `maxProxyCostUsd` and `maxSpendUsd` still have room. The first search stays direct. The proxy group comes from `proxyConfiguration` or `proxyGroups`; an empty group uses datacenter. A CAPTCHA solve runs only when that retry is still a challenge, `allowCaptcha` is true, and the spend cap covers the solve. This Actor does not ship a solver, so `allowCaptcha: true` still pushes `blocked` and does not bill a solve until one is configured. When the cap is already spent, solving is off, or the proxy URL cannot be opened, the current query is pushed as `blocked`, remaining queries are skipped, and the run exits successfully. A robots disallow is not retried. Chromium starts with `--disable-blink-features=AutomationControlled` from `BusinessDiscoverySession`. No stealth plugin is added. Search and detail delays stay on the source session.

### Limitations

- One Maps page of results per query, then place pages up to `maxResults`.
- Official Places API data (opening hours from the API, verified owner fields) is out of scope.
- Emails are not collected for `low_confidence` or `no_match` rows.
- ZIP-to-county and area-code checks use the tables in `scripts/lead_automation/data/` when those files are present.

### Pricing

Pay per event. The primary event is `result` at $0.05, charged only for `decision: "match"`. `low_confidence`, `no_match`, `blocked`, and `error` rows are stored and not billed. `proxy` is $0.01 and is charged only when a blocked query is retried through Apify Proxy. `captcha` is $1.00 and is charged only when a solver returns a token. Direct searches are free. Dataset items are not a charge event, so a rejected business does not cost a result. `maxSpendUsd` (default 5) caps those events together. `maxProxyCostUsd` (default 0.50) caps proxy events. The platform still applies its start event. Prices are the starting Store rates in `.actor/pay_per_event.json`. A local run logs that the charge was ignored until the Console pricing model is pay per event; the dataset row is still saved. Default memory is 2048 MB and the default timeout is 900 seconds. Do not price this as a bulk crawler.

### Categories

Set in the Apify Console. Suggested: Business, Lead generation.

### Local run

```bash
cd scripts/apify-scripts/google-business
mkdir -p storage/key_value_stores/default
cp fixtures/INPUT.json storage/key_value_stores/default/INPUT.json
apify run --purge
```

A local run opens Google Maps. Unit tests do not. Publish with `APIFY_TOKEN`, not `APIFY_API_KEY`. See `scripts/apify-scripts/README.md`.

# Changelog

This Actor's version history is a separate document: https://apify.com/staticxviper/google-business-lead-generation/changelog.md

# Actor input Schema

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

Objects with name, state, and optional city, county, and maxResults.

## `name` (type: `string`):

Single-query name, used when queries is empty.

## `city` (type: `string`):

Optional city for the single-query form.

## `county` (type: `string`):

Optional county for the single-query form. Do not include the word County.

## `state` (type: `string`):

Required two-letter state code for the single-query form.

## `maxResults` (type: `integer`):

Listings to open per query. Default 5, maximum 20.

## `minConfidence` (type: `integer`):

Score required for a match. Default 80.

## `lowConfidenceFloor` (type: `integer`):

Scores at or above this floor and below the minimum are low_confidence. Default 60.

## `maxSearches` (type: `integer`):

How many queries to run. Default 10, maximum 50.

## `includeEmails` (type: `boolean`):

When true, emails are read only from the website of a match. Addresses are validated. Nothing is guessed.

## `searchDelayMin` (type: `number`):

Optional override. Default range is 2.0 to 4.5.

## `searchDelayMax` (type: `number`):

Optional override. Default range is 2.0 to 4.5.

## `detailDelayMin` (type: `number`):

Optional override. Default range is 1.2 to 2.8.

## `detailDelayMax` (type: `number`):

Optional override. Default range is 1.2 to 2.8.

## `headless` (type: `boolean`):

Run Chromium headless. Default true.

## `maxSpendUsd` (type: `number`):

Stop when the next result, proxy, or CAPTCHA event would pass this amount. Default 5, maximum 100. Later rows are saved and are not billed.

## `maxProxyCostUsd` (type: `number`):

Cap on proxy events alone. Default 0.50, clamped to maxSpendUsd. The first Maps search stays direct. A proxy retry happens only after a block, and only when this cap and maxSpendUsd both still have room. 0 skips the proxy retry.

## `allowCaptcha` (type: `boolean`):

Default false. When false, a challenge after the proxy retry pushes a blocked row and exits without a solve charge. When true, a solve is attempted only if that proxy retry is still a challenge and maxSpendUsd covers the captcha event. This Actor does not ship a solver, so an unconfigured solve pushes blocked and is not billed.

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

Apify Proxy group used only after Google returns a block page. Leave Apify Proxy off so the first search stays direct. Empty groups use datacenter. A robots disallow is not retried.

## `proxyGroups` (type: `array`):

Optional group names, such as DATACENTER or RESIDENTIAL. Used after a block when proxyConfiguration has no apifyProxyGroups.

## Actor input object example

```json
{
  "queries": [
    {
      "name": "Wegmans Food Markets",
      "city": "Rochester",
      "county": "Monroe",
      "state": "NY",
      "maxResults": 3
    }
  ],
  "maxResults": 5,
  "minConfidence": 80,
  "lowConfidenceFloor": 60,
  "maxSearches": 10,
  "includeEmails": false,
  "headless": true,
  "maxSpendUsd": 5,
  "maxProxyCostUsd": 0.5,
  "allowCaptcha": false,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

Dataset containing all scraped items

## `files` (type: `string`):

Key-value store containing the SUMMARY record for 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 = {
    "queries": [
        {
            "name": "Wegmans Food Markets",
            "city": "Rochester",
            "county": "Monroe",
            "state": "NY",
            "maxResults": 3
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("staticxviper/google-business-lead-generation").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 = { "queries": [{
            "name": "Wegmans Food Markets",
            "city": "Rochester",
            "county": "Monroe",
            "state": "NY",
            "maxResults": 3,
        }] }

# Run the Actor and wait for it to finish
run = client.actor("staticxviper/google-business-lead-generation").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 '{
  "queries": [
    {
      "name": "Wegmans Food Markets",
      "city": "Rochester",
      "county": "Monroe",
      "state": "NY",
      "maxResults": 3
    }
  ]
}' |
apify call staticxviper/google-business-lead-generation --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,staticxviper/google-business-lead-generation"
        }
    }
}
```

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/V2brtmw1y9fNnmHCu/builds/uckfa5J5rZhulGNnd/openapi.json
