# Google Maps Lead Scraper & SMB Audit (`scrapenode/google-maps-lead-scraper-smb-audit`) Actor

Fast, pure-HTTP Google Maps scraper to extract leads, phone numbers, and websites for local business prospecting. Automatically audits online presence and calculates SMB outreach priority scores without running a heavy browser.

- **URL**: https://apify.com/scrapenode/google-maps-lead-scraper-smb-audit.md
- **Developed by:** [Muhammad Sajid Izzulhaq](https://apify.com/scrapenode) (community)
- **Stats:** 3 total users, 2 monthly users, 98.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 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 Website-Lead Finder

An Apify actor that finds **local businesses without a website** on Google Maps and ranks them by priority, so you know exactly whom to contact first.

Best for: freelance web developers, digital agencies, and anyone selling website or landing page services to small local businesses.

### The problem it solves

Looking for prospects manually on Google Maps is exhausting. Search for "coffee shop" and on average 19 out of 20 results **have no website at all** — and most have no reachable phone number either. This actor filters out both problems in one pass and flags which businesses are worth chasing.

### What the actor does

1. Pulls Google Maps search results through Google's internal JSON endpoint (`tbm=map`). No headless browser, so it is much faster and cheaper.
2. Extracts name, address, rating, review count, category, coordinates, **phone number**, and website.
3. Computes a `gapScore` from 0-100 for every business, with a ready-to-use pitch reason you can read aloud when calling.
4. Returns only viable leads: businesses with **no own website** **and** a public phone number.

### Input

`location` is not a decoration: the city name there **moves the search center**. Google's geographic filter lives in `pb` under `!2d{lng}!3d{lat}`; if the city is not mapped to the right coordinates, you get results from the wrong city without any warning.

How the actor handles `location`:

1. it matches the name against an offline city table (819 entries: 217 Indonesian cities + major world cities), ignoring case, spaces, and the `kota` prefix;
2. it uses the match as the search center point, not just as an extra query word;
3. it also warns when `countryCode` doesn't match that city's country.

If the city name is not in the table, the actor **stops with a clear message** — failing is better than delivering leads from the wrong city. If the place you need is missing, pass `lat`/`lng` directly, or add the city to `packages/gmaps-core/src/gmaps_core/data/cities.json`.

| Field | Type | Default | Description |
|---|---|---|---|
| `searchStrings` | list of string | `["Kopi"]` | Keywords. The actor combines them with `location`. |
| `location` | string | **required** | The city used as the scan center, e.g. `Jakarta`. Matched against the offline city table. |
| `lat` / `lng` | number | empty | Manual center point. Leave empty unless you know the exact coordinates. |
| `radiusKm` | number | `30` | Radius around the center point. |
| `maxItems` | integer | `20` | Total results per keyword (1-480). Automatically split into multiple requests. |
| `language` | string | `id` | Google's response language code, e.g. `id` or `en`. |
| `countryCode` | string | `id` | Google's response country code, e.g. `id` or `jp`. |
| `onlyGapLeads` | boolean | `true` | Only leads without a website and with a phone number. |
| `minGapScore` | integer | `0` | Drop leads below this score. |
| `includeUnproven` | boolean | `false` | Include businesses without any rating. |
| `includeRaw` | boolean | `false` | Return every place, not just leads. |
| `debugDump` | boolean | `false` | Save the raw Google payload to the key-value store. |

### Output

Three outputs at once:

| Destination | Location on the run page | Contents |
|---|---|---|
| Spreadsheet | key-value store default, record `leads.xlsx` | 2 sheets: `Leads` and `Ringkasan`. |
| Raw data | dataset | One row per place, filterable in the Apify UI. |
| Run summary | key-value store default, record `SUMMARY` | Run statistics and parameters. |

The dataset has two views, chosen from the dropdown above the table:

| View | Contents |
|---|---|
| `Leads` | 15 columns for contacting prospects, sorted from the highest score. |
| `Semua tempat` (All places) | All 24 columns, including businesses that already have their own website. |

Column headers in the exported files are in Indonesian, and the `Prioritas` (Priority) column uses `Panas` / `Hangat` / `Dingin` (Hot / Warm / Cold) — the English values only matter inside the code.

#### Spreadsheet contents

The `Leads` sheet contains every scanned place (not just leads), sorted by score — handy because the people who need to filter it can use the Autofilter dropdown:

`No`, `Nama` (Name), `Kategori` (Category), `Rating`, `Ulasan` (Reviews), `Telepon` (Phone), `WhatsApp`, `Kota` (City), `Kecamatan` (District), `Alamat` (Address), `Website`, `Skor Gap` (Gap Score), `Prioritas` (Priority), `Alasan (script jualan)` (Reason - pitch script), `Buka di Maps` (Open in Maps), `Penawaran` (Offer).

Some deliberate details:

- **Phone numbers are stored as text.** If they were numbers, Excel would drop leading zeros and the number would be undialable.
- **The WhatsApp column is a ready-to-click `wa.me` link**, but only for mobile numbers. Landline numbers can't be used with WhatsApp, so they're left empty.
- **Reason and Offer columns are in Indonesian**, ready to be read aloud when calling.

The `Ringkasan` (Summary) sheet contains the run configuration, the number of leads per priority, and how many have a phone number.

#### Dataset fields

| Field | Description |
|---|---|
| `title`, `address`, `rating`, `review_count`, `category` | Basic business data. |
| `phone`, `phoneInternational`, `phoneDigits` | Phone number in three formats. |
| `website` | Website URL if any. |
| `websiteKind` | `own` (has its own website), `social` (social account only), `none`. |
| `gapScore` | 0-100. Higher = more worth pursuing. |
| `gapTier` | `hot` / `warm` / `cold` / `none`. |
| `isGapLead` | `true` when worth contacting for website services. |
| `gapReason` | Reason in Indonesian, ready to read to the prospect. |
| `suggestedOffer` | Recommended offer angle. |
| `place_url`, `reviews_url`, `lat`, `lng` | For manual verification. |

### How the score is calculated

Three components, all transparent:

| Component | Points | Logic |
|---|---|---|
| Digital gap | 0-40 | No web (40), social only (32), already has a website (0). |
| Reputation | 0-47 | Rating only counts above 3.5; reviews use a logarithmic scale. |
| Contactability | 0-15 | Has a public phone number. |

Two deliberate adjustments:

- **Ratings below 3.5 score 0 points.** A weak business is a risk, not an opportunity.
- **Very high review counts (>3,000) lose 8 points.** The business is already established, so selling a personal website service is harder.

### Example output

```json
{
  "no": 1,
  "title": "BARBIE LAUNDRY",
  "address": "Jl. Puntik Kemuning No.88, Peanut, Tj. Karang Timur",
  "rating": 4.7,
  "review_count": 464,
  "phone": "0812-7923-7715",
  "phoneInternational": "+62 812-7923-7715",
  "phoneDigits": "081279237715",
  "website": "",
  "websiteKind": "none",
  "hasPhone": true,
  "hasWebsite": false,
  "contactable": true,
  "gapScore": 93,
  "gapTier": "hot",
  "isGapLead": true,
  "gapReason": "Tidak punya website sama sekali. Reputasi sudah bagus (rating 4.7, 464 ulasan). Ada nomor telepon publik sehingga bisa langsung dihubungi.",
  "suggestedOffer": "Website + Google Business Profile + landing page promo - tawarkan social proof dari ulasan yang sudah banyak"
}
```

### Example run

```bash
apify run umkm-google-maps \
  --input '{"searchStrings":["warung kopi","laundry"],"location":"Bandar Lampung","maxItems":40}'

## Different city
apify run umkm-google-maps \
  --input '{"searchStrings":["kopi"],"location":"Jakarta","maxItems":100}'

## Foreign city: adjust the country code, otherwise the Reviews column
## follows the Indonesian language
apify run umkm-google-maps \
  --input '{"searchStrings":["coffee"],"location":"Tokyo","countryCode":"jp","language":"en"}'
```

### Proxy (optional)

If you hit rate limits, the actor can scrape through a proxy. Credentials are **never placed in the input schema** — only passed through the `SCRAPER_PROXY` environment variable in `http://user:pass@host:port` format.

For Apify runs, set it via **Console → this actor → Environment variables**, not through `actor.json` and not through run input. The `actor.json` value is only used when deploying with the Apify CLI; deploys from GitHub take the value from Console.

For local runs, just set it in the shell:

```bash
export SCRAPER_PROXY="http://user:pass@host:port"
```

If empty, the actor runs without a proxy.

### Technical notes

Scraping uses `curl_cffi` to spoof a Chrome-like TLS fingerprint. Without it, Google blocks the request at the network level. Rate limiting of 1.5-3 seconds with exponential backoff is baked into the code and cannot be turned off.

Google's field index structure lives in `packages/gmaps-core/src/gmaps_core/indexes.py` — the only place you need to touch if Google changes its response.

### Limitations

- Maximum 40 results per request (Google's limit), so `maxItems` is split automatically. Hard cap for `maxItems` = 480 per keyword. If you get fewer results than `maxItems`, it means there really are no more places in that area — the scrape did not fail.
- `location` must be the **name of a city in the table**. Street addresses, formatted names, or subdistrict names (`Kemang`, `Kemang, Jatiasih`) are not recognized. Use `lat`/`lng` for those cases.
- Phone numbers only exist when the business owner publicly posted them on their Google Business profile. The actor never guesses or makes up numbers.
- Data comes from Google Maps. Respect Google's terms of service, and do not use this actor to collect personal data the business owner has not published.

# Actor input Schema

## `searchStrings` (type: `array`):

Satu atau beberapa kata kunci. Contoh: 'Kopi', 'laundry', 'bengkel motor'. Actor akan menggabungkan tiap kata kunci dengan nilai Lokasi di bawah.

## `location` (type: `string`):

Nama kota yang jadi titik tengah pemindaian, contoh 'Bandar Lampung' atau 'Jakarta'. Dicocokkan ke tabel kota offline (Indonesia + kota besar dunia, ~800 nama), jadi tidak perlu mencari koordinat sendiri. Tulis 'Kota, Negara' kalau nama-nya bentrok, contoh 'San Jose, United States'. Nama yang tidak ada di tabel akan ditolak, bukan ditebak diam-diam. Kalau diisi Latitude dan Longitude, keduanya mengabaikan nilai ini.

## `lat` (type: `number`):

Kosongkan kecuali sudah tahu persis koordinat yang diinginkan. Kalau diisi, wajib berpasangan dengan Longitude, dan nilai Lokasi diabaikan sebagai pusat.

## `lng` (type: `number`):

Kosongkan kecuali sudah tahu persis koordinat yang diinginkan. Kalau diisi, wajib berpasangan dengan Latitude. Contoh Jakarta: lat -6.2088, lng 106.8456.

## `radiusKm` (type: `number`):

Radius dari titik tengah. Makin besar = makin banyak hasil, makin lambat.

## `maxItems` (type: `integer`):

Batas jumlah hasil yang dikembalikan per kata kunci.

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

Kode bahasa respons Google, contoh 'id' atau 'en'.

## `countryCode` (type: `string`):

Kode negara respons Google, contoh 'id' atau 'us'.

## `onlyGapLeads` (type: `boolean`):

Jika true, hanya hasil yang tidak punya website sendiri DAN punya nomor telepon yang dikembalikan. Ini yang Anda butuhkan untuk lead generation.

## `minGapScore` (type: `integer`):

Buang lead dengan skor di bawah nilai ini (0-100).

## `includeUnproven` (type: `boolean`):

Jika true, bisnis yang belum punya ulasan Google ikut disertakan. Default false karena belum terbukti.

## `includeRaw` (type: `boolean`):

Jika true, semua tempat dikembalikan beserta penanda gapScore, sehingga Anda bisa menyaring sendiri di luar actor.

## `debugDump` (type: `boolean`):

Simpan payload Google apa adanya ke storage. Nyalakan hanya kalau hasil terlihat aneh.

## Actor input object example

```json
{
  "searchStrings": [
    "Kopi"
  ],
  "location": "Bandar Lampung",
  "radiusKm": 30,
  "maxItems": 20,
  "language": "id",
  "countryCode": "id",
  "onlyGapLeads": true,
  "minGapScore": 0,
  "includeUnproven": false,
  "includeRaw": false,
  "debugDump": false
}
```

# Actor output Schema

## `leads` (type: `string`):

Semua hasil pindai UMKM di Google Maps, termasuk penilaian celah digital, alasan, dan saran penawaran.

# 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 = {
    "searchStrings": [
        "Kopi"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapenode/google-maps-lead-scraper-smb-audit").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 = { "searchStrings": ["Kopi"] }

# Run the Actor and wait for it to finish
run = client.actor("scrapenode/google-maps-lead-scraper-smb-audit").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 '{
  "searchStrings": [
    "Kopi"
  ]
}' |
apify call scrapenode/google-maps-lead-scraper-smb-audit --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scrapenode/google-maps-lead-scraper-smb-audit"
        }
    }
}
```

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/ey2Odeuc3dOfBslzb/builds/g0uG9csQLP7pSpKd3/openapi.json
