# Google Maps Leads Scraper (`hasnainnisar67/google-maps-leads-scraper`) Actor

Extract qualified B2B leads from Google Maps by search query and location. Returns business name, address, phone, website, rating, review count, category, coordinates, and emails harvested from the linked website. Built for outbound sales and territory research.

- **URL**: https://apify.com/hasnainnisar67/google-maps-leads-scraper.md
- **Developed by:** [Hasnain Nisar](https://apify.com/hasnainnisar67) (community)
- **Categories:** Lead generation, Automation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, NaN 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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are a software tools running on the Apify platform, for all kinds of web data extraction and automation use cases.
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.

In JavaScript/TypeScript projects, use official [JavaScript/TypeScript client](https://docs.apify.com/api/client/js.md):

```bash
npm install apify-client
```

In Python projects, use official [Python client library](https://docs.apify.com/api/client/python.md):

```bash
pip install apify-client
```

In shell scripts, use [Apify CLI](https://docs.apify.com/cli/docs.md):

````bash
# MacOS / Linux
curl -fsSL https://apify.com/install-cli.sh | bash
# Windows
irm https://apify.com/install-cli.ps1 | iex
```bash

In AI frameworks, you might use the [Apify MCP server](https://docs.apify.com/platform/integrations/mcp.md).

If your project is in a different language, use 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 Scraper — Apify Actor

A drop-in Apify actor that turns either a **natural-language prompt** ("find 50 dentists in Dubai with email") or a list of **search queries** into a clean dataset of business listings: name, address, phone, website, rating, review count, category, lat/lng — plus, with **deep scan** enabled, emails, phone numbers, social-media links, and named contacts harvested from each business's website.

Built on Playwright + Chromium with Apify Proxy support and Groq-powered prompt parsing.

### What it does

For each search term (either typed in directly or extracted from your prompt), the actor:

1. Opens `https://www.google.com/maps/search/<term> in <location>`
2. Scrolls the left-hand results panel until either `maxResults` cards have loaded or Google's *"You've reached the end of the list"* sentinel appears
3. Clicks each card and extracts the structured fields from the detail pane
4. **(Deep scan)** Visits the listing's website + up to two contact-style sub-pages, harvesting emails, phones, social URLs, and named contacts
5. Applies your filters (`minRating`, `hasEmail`, `noWebsite`, etc.) and pushes each kept record to the dataset **as soon as it's produced** — partial runs stay useful

### Two ways to drive it

#### 1. Prompt mode (recommended)

Just describe what you want:

```json
{
    "prompt":     "find 50 dentists in Dubai with email and rating above 4",
    "groqApiKey": "<your-groq-key>",
    "deepScan":   true
}
````

The actor calls Groq's `llama-3.3-70b-versatile` to parse that into:

```json
{
    "query":         "Dentists in Dubai, United Arab Emirates",
    "city":          "Dubai",
    "country":       "United Arab Emirates",
    "category":      "Dentists",
    "suggested_max": 50,
    "deep_scan":     true,
    "filters": {
        "has_email":  true,
        "min_rating": 4.0
    },
    "priority_fields": ["email"]
}
```

Then runs the scrape with those settings. **No Groq key?** A built-in rule-based parser handles the basics ("50 plumbers in NYC", "dentists in London with phone") without any LLM call — accuracy dips for ambiguous phrasings, but it's free and offline.

You can still override individual fields. If you supply both `prompt: "100 dentists"` and `searchTerms: ["orthodontists"]`, the explicit `searchTerms` wins. This lets you use the prompt as a starting template and tweak pieces.

#### 2. Direct mode

Skip the prompt and pass everything explicitly:

```json
{
    "searchTerms":  ["dentists", "orthodontists"],
    "location":     "Austin, TX",
    "maxResults":   30,
    "deepScan":     true,
    "filters":      { "minRating": 4.5 }
}
```

### Deep scan — what it pulls

When `deepScan` is on, every listing with a website is visited. The crawler extracts:

| Field | Source |
|---|---|
| `deepScanEmails` | All `mailto:` links + email-shaped strings on homepage and `/contact`-style sub-pages |
| `deepScanPhones` | All `tel:` links + phone-shaped strings (filtered to avoid date/license-number noise) |
| `deepScanSocials` | Instagram, Facebook, LinkedIn, X/Twitter, TikTok, YouTube, WhatsApp links |
| `deepScanIndividuals` | Names following titles like "CEO:", "Founder:", "Director:", "Owner:" |

Each is a comma-separated string for spreadsheet-friendly output. Empty when the field couldn't be resolved.

**Performance:** ~3-8 seconds per listing with a website, with an early-exit once both an email and a phone have been found.

### Output schema

Each row in the dataset:

```json
{
    "name":          "Joe's Coffee",
    "category":      "Coffee shop",
    "address":       "123 W 56th St, New York, NY 10019",
    "phone":         "+1 212-555-0142",
    "website":       "https://joescoffee.example",
    "email":         "hello@joescoffee.example",
    "rating":        4.6,
    "reviewsCount":  812,
    "hours":         "Open ⋅ Closes 8 PM",
    "plusCode":      "Q263+CV New York, NY",
    "latitude":      40.762435,
    "longitude":     -73.978942,
    "googleMapsUrl": "https://www.google.com/maps/place/Joe's+Coffee/data=...",
    "searchTerm":    "coffee shops",

    "deepScanEmails":      "hello@joescoffee.example, orders@joescoffee.example",
    "deepScanPhones":      "+12125550142, +12125550143",
    "deepScanSocials":     "https://instagram.com/joescoffee, https://facebook.com/joescoffee",
    "deepScanIndividuals": "John Doe, Jane Smith"
}
```

`deepScan*` fields are `""` unless deep scan is enabled. Any field not present on the listing comes back as an empty string or `null`.

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `prompt` | `string` | `""` | Natural-language description. When set, parsed values fill in `searchTerms`/`location`/`maxResults`/`filters`/`deepScan` for slots you didn't explicitly provide. |
| `searchTerms` | `string[]` | `["coffee shops"]` | Used when `prompt` is empty (or to override the parser's category). Each term is searched independently. |
| `location` | `string` | `"New York"` | City / region / country. Combined with each term as `<term> in <location>`. Leave empty to use the term verbatim. |
| `maxResults` | `integer` | `50` | Hard cap **per search term**. Maps caps result lists around 120; values above are silently clamped. Prompt's "find X" is honored when this is left at the default. |
| `language` | `enum` | `"en"` | Two-letter language code → Google's `hl` parameter. |
| `deepScan` | `boolean` | `false` | Visit each listing's website to extract emails, phones, socials, named contacts. Required for `hasEmail`/`hasWhatsapp` filters to work reliably. |
| `extractEmails` | `boolean` | `false` | Lightweight email-only lookup. Ignored when `deepScan` is on. |
| `extractWebsiteFallback` | `boolean` | `true` | When `extractEmails` is on, also try `<website>/contact` if the homepage has no email. |
| `filters` | `object` | `{}` | Post-scrape filter dict — see below. |
| `groqApiKey` | `string` (secret) | `""` | Required for AI prompt parsing. Free at https://console.groq.com. Without one, a rule-based fallback is used. |
| `headless` | `boolean` | `true` | Disable only for local debugging. |
| `proxy` | `proxy` | `{ useApifyProxy: true }` | RESIDENTIAL group strongly recommended. |

#### Filter object

```json
{
    "noWebsite":   false,
    "hasPhone":    false,
    "hasEmail":    false,
    "hasWhatsapp": false,
    "minRating":   null,
    "maxRating":   null,
    "minReviews":  null
}
```

All `null` / `false` filters are inactive. `hasEmail`, `hasPhone`, `hasWhatsapp` will look in the deep-scan results too when those fields are populated.

### Usage

#### From the Apify console

1. Open the actor's **Run** tab.
2. Either type a prompt + paste your Groq key, or fill in `searchTerms` / `location` / `maxResults`.
3. Toggle **Deep scan** if you want emails / phones / socials.
4. **Start** — watch records stream into the dataset as they're produced.

#### From the Apify CLI

```bash
apify call <username>/google-maps-scraper --input='{
    "prompt": "find 100 plumbers in Brooklyn with email rating above 4",
    "groqApiKey": "gsk_...",
    "deepScan": true
}'
```

#### Programmatically (Apify client SDK)

```python
from apify_client import ApifyClient

client = ApifyClient("<your-apify-token>")
run = client.actor("<username>/google-maps-scraper").call(run_input={
    "prompt":     "50 dental clinics in Karachi Pakistan with phone",
    "groqApiKey": "gsk_...",
    "deepScan":   True,
})
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item["name"], "—", item["phone"], "—", item.get("deepScanEmails", ""))
```

### Run locally

```bash
cd apify-google-maps-actor

## 1) Install deps + Playwright browser
pip install -r requirements.txt
python -m playwright install chromium

## 2) Provide input via Apify's local-storage convention
mkdir -p storage/key_value_stores/default
cat > storage/key_value_stores/default/INPUT.json <<'EOF'
{
    "prompt":     "10 coffee shops in NYC",
    "deepScan":   false,
    "headless":   false
}
EOF

## 3) Run
python -m src
```

Records are written to `storage/datasets/default/` as numbered `.json` files.

### Build & deploy to Apify

```bash
cd apify-google-maps-actor
apify login        ## one-time
apify push         ## builds the Dockerfile remotely & deploys
```

Or via Git: push this folder to a GitHub repo, then point a new actor on Apify at that repo — the platform builds from `.actor/Dockerfile`.

### Project layout

```
apify-google-maps-actor/
├── .actor/
│   ├── actor.json            ## actor metadata + dataset views (Overview + Deep scan)
│   ├── input_schema.json     ## 12 input fields rendered as the run form
│   └── Dockerfile            ## builds on apify/actor-python-playwright:3.12
├── src/
│   ├── __init__.py
│   ├── __main__.py           ## entrypoint — Apify lifecycle, prompt parsing, term loop
│   ├── scraper.py            ## Playwright-based Maps scrape (extract + enrich + keep)
│   ├── prompt_parser.py      ## natural-language prompt → structured config (Groq + fallback)
│   ├── website_crawler.py    ## deep-scan: emails, phones, socials, named contacts
│   └── filters.py            ## post-scrape filter predicate (rating, reviews, has_phone, ...)
├── requirements.txt
├── .dockerignore
├── .gitignore
└── README.md (this file)
```

### Notes & limits

- **Google rate-limiting.** Without a proxy, expect blocks after a few hundred requests from one IP. Apify Proxy → RESIDENTIAL is the practical default.
- **Maps result caps.** Google itself stops listing results around the 120-mark. Splitting one big search into several narrower queries (by neighborhood instead of city) gets past that ceiling.
- **Deep-scan coverage.** ~30-60% of small-business websites expose an email; the rest hide behind contact forms. The actor never invents an email — `deepScanEmails` is just empty.
- **Groq parsing.** The `llama-3.3-70b-versatile` model handles ambiguous prompts (locale-specific country/city splits, multiple filters per sentence) much better than the rule-based fallback. Get a free key at https://console.groq.com.
- **Prompt vs explicit input.** Explicit fields always win. The prompt fills in only what you left blank — so you can use the prompt as a base template and override pieces.

### License

MIT — same as the parent project.

# Actor input Schema

## `prompt` (type: `string`):

Describe what you want in plain English — the actor parses it into a structured query, location, lead count, and filter set. Examples: "find 50 dentists in Dubai with email", "plumbers in Karachi Pakistan rating above 4", "100 coffee shops in NYC no website". When provided, the parsed values OVERRIDE `searchTerms`/`location`/`maxResults`/individual filters/`deepScan` unless those are also explicitly set. AI parsing uses the actor's pre-configured Groq API key; a rule-based fallback parser kicks in if it's missing.

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

One or more search queries to run on Google Maps. Each term is searched independently and results are merged into one dataset. Combined with `placeCategories` if both are set. Ignored when `prompt` is set, or when `placeUrls`/`placeIds` are supplied.

## `placeCategories` (type: `array`):

Optional category list — the actor expands each searchTerm × category combination. Example: searchTerms=\["luxury"], placeCategories=\["hotels","restaurants"] → searches "luxury hotels" and "luxury restaurants". Leave empty to use search terms verbatim.

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

Direct Maps URLs to scrape. RECOMMENDED FORMAT: `https://www.google.com/maps/place/?q=place_id:<id>` — works reliably. The `/maps/place/<Name>/data=!4m7!...` form that Google's 'Share → Copy link' produces is unreliable from headless browsers (Maps doesn't fully render the detail pane on cold-start). If you have one of those, prefer using `placeIds` below — the actor converts those to the working URL form automatically.

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

Google Place IDs (e.g. `ChIJN1t_tDeuEmsRUsoyG83frY4`). Each is converted to a Maps URL and scraped exactly like `placeUrls`. Combine freely with `placeUrls`.

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

City, region, or country to scope the search to. Combined with each search term as "<term> in <location>". Leave empty to fall back to the structured Country/City/State fields below.

## `country` (type: `string`):

Used only when free-text Location above is empty. Combined with City/State/County/Postal code into a single location string.

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

Used only when free-text Location is empty.

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

Used only when free-text Location is empty.

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

Used only when free-text Location is empty. Rarely needed but kept for parity with admin hierarchies.

## `postalCode` (type: `string`):

Used only when free-text Location is empty.

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

When filters are off, this is the search cap. When filters are active, the actor over-discovers (up to 3×, capped at 25 URLs) and stops scraping the moment enough records pass — so this becomes an output target.

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

Two-letter language code passed to Google Maps via the `hl` parameter. Affects business names, categories, and review content.

## `deepScan` (type: `boolean`):

ON by default. The actor visits every listing's website (homepage + a couple of contact-style sub-pages) to harvest emails, phone numbers, social-media links, and named contacts. The best email/phone is also promoted into the top-level `email` / `phone` fields. Adds ~3-8 seconds per listing that has a website.

## `extractEmails` (type: `boolean`):

Lightweight fallback for when Deep scan is disabled: fetches the homepage and standard contact paths over plain HTTP (no browser) looking for an email. Ignored when Deep scan is on, since deep scan is strictly more thorough.

## `extractWebsiteFallback` (type: `boolean`):

Only used by Quick email lookup (when Deep scan is OFF). Doubles per-site latency for sites that don't expose an email at all.

## `verifyEmails` (type: `boolean`):

Runs a syntax check + DNS MX-record lookup on every email the actor produces. Adds an `emailValid` boolean to each record. Adds ~50-200 ms per unique domain (cached). Recommended for cold-outreach lists.

## `reviewsLimit` (type: `integer`):

0 = skip reviews scraping entirely. Up to ~200 per place is reasonable; higher counts substantially increase per-place runtime (≈1 s per 10 reviews scrolled). Output appears in a `reviews` array on each record.

## `reviewsSort` (type: `string`):

How Maps orders the reviews list before we read it. `newest` is best for tracking; `mostRelevant` is the Maps default; `highestRating`/`lowestRating` for sentiment slicing.

## `reviewsSince` (type: `string`):

Optional cutoff. Reviews older than this date are dropped. Maps shows reviews as relative time ("3 weeks ago") so absolute dates are estimated; off-by-a-day is possible.

## `reviewsKeywords` (type: `string`):

If set, only reviews whose text contains at least one of these terms (case-insensitive) are kept. Empty = no keyword filter.

## `includeReviewerData` (type: `boolean`):

When on, each review carries the reviewer's display name, profile URL, and total-reviews count where Maps exposes them.

## `imagesLimit` (type: `integer`):

0 = main photo only (free, always included as `mainImageUrl`). Higher numbers open the photo gallery and harvest URLs. Output appears in an `images` array on each record. The first ~30 images load fast; beyond that requires extra scrolling.

## `includeImageAuthors` (type: `boolean`):

Adds the photo uploader's display name to each image entry where Maps shows it.

## `noWebsite` (type: `boolean`):

Keep only records whose `website` field is empty.

## `hasWebsite` (type: `boolean`):

Mutually exclusive with the previous toggle.

## `hasPhone` (type: `boolean`):

Drop records with no phone (Google Maps `phone` field, or `deepScanPhones` if Deep scan is on).

## `hasEmail` (type: `boolean`):

Drop records with no email. Without Deep scan, only the Quick-email-lookup result is checked.

## `hasWhatsapp` (type: `boolean`):

Keep only records whose deep-scanned social links include `wa.me` or `whatsapp.com`. Effectively requires Deep scan.

## `skipClosedPlaces` (type: `boolean`):

Drop records flagged with "Permanently closed" or "Temporarily closed" on Maps.

## `exactNameMatch` (type: `boolean`):

Maps's relevance ranking can return loosely-related places. Enable this to drop records whose `name` doesn't contain at least one of the search terms (case-insensitive). No effect when running from `placeUrls`/`placeIds`.

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

Drop records with rating below this value. Records with no rating at all are also dropped. Leave blank for no minimum.

## `maxRating` (type: `number`):

Drop records with rating above this value. Leave blank for no maximum.

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

Drop records with fewer reviews than this. Leave blank for no minimum.

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

Disable only for debugging on a local run — Apify's servers are headless regardless.

## `proxy` (type: `object`):

Use Apify Proxy or your own custom proxies. Strongly recommended — Google rate-limits scrapers from any single IP after a few hundred requests. RESIDENTIAL group is safest.

## Actor input object example

```json
{
  "searchTerms": [
    "coffee shops"
  ],
  "placeCategories": [],
  "placeUrls": [],
  "placeIds": [],
  "location": "New York",
  "maxResults": 50,
  "language": "en",
  "deepScan": true,
  "extractEmails": false,
  "extractWebsiteFallback": true,
  "verifyEmails": false,
  "reviewsLimit": 0,
  "reviewsSort": "newest",
  "includeReviewerData": true,
  "imagesLimit": 0,
  "includeImageAuthors": false,
  "noWebsite": false,
  "hasWebsite": false,
  "hasPhone": false,
  "hasEmail": false,
  "hasWhatsapp": false,
  "skipClosedPlaces": false,
  "exactNameMatch": false,
  "headless": true,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

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

One business listing per item — name, address, phone, website, email, rating, reviews count, lat/lng, and (when enabled) deep-scanned contact details, reviews, and images.

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

Final tally of records, active filters, and trial/billing status. Lives at key 'SUMMARY' in the default key-value store.

# 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 = {
    "prompt": "",
    "searchTerms": [
        "coffee shops"
    ],
    "placeCategories": [],
    "placeUrls": [],
    "placeIds": [],
    "location": "New York",
    "reviewsSince": "",
    "reviewsKeywords": "",
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("hasnainnisar67/google-maps-leads-scraper").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 = {
    "prompt": "",
    "searchTerms": ["coffee shops"],
    "placeCategories": [],
    "placeUrls": [],
    "placeIds": [],
    "location": "New York",
    "reviewsSince": "",
    "reviewsKeywords": "",
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("hasnainnisar67/google-maps-leads-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "prompt": "",
  "searchTerms": [
    "coffee shops"
  ],
  "placeCategories": [],
  "placeUrls": [],
  "placeIds": [],
  "location": "New York",
  "reviewsSince": "",
  "reviewsKeywords": "",
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call hasnainnisar67/google-maps-leads-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=hasnainnisar67/google-maps-leads-scraper",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

```json
{
    "openapi": "3.0.1",
    "info": {
        "title": "Google Maps Leads Scraper",
        "description": "Extract qualified B2B leads from Google Maps by search query and location. Returns business name, address, phone, website, rating, review count, category, coordinates, and emails harvested from the linked website. Built for outbound sales and territory research.",
        "version": "0.2",
        "x-build-id": "geKMcyoF0OoyWmOu9"
    },
    "servers": [
        {
            "url": "https://api.apify.com/v2"
        }
    ],
    "paths": {
        "/acts/hasnainnisar67~google-maps-leads-scraper/run-sync-get-dataset-items": {
            "post": {
                "operationId": "run-sync-get-dataset-items-hasnainnisar67-google-maps-leads-scraper",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for its completion, and returns Actor's dataset items in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        },
        "/acts/hasnainnisar67~google-maps-leads-scraper/runs": {
            "post": {
                "operationId": "runs-sync-hasnainnisar67-google-maps-leads-scraper",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor and returns information about the initiated run in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/runsResponseSchema"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/acts/hasnainnisar67~google-maps-leads-scraper/run-sync": {
            "post": {
                "operationId": "run-sync-hasnainnisar67-google-maps-leads-scraper",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for completion, and returns the OUTPUT from Key-value store in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        }
    },
    "components": {
        "schemas": {
            "inputSchema": {
                "type": "object",
                "properties": {
                    "prompt": {
                        "title": "Natural-language prompt (optional)",
                        "type": "string",
                        "description": "Describe what you want in plain English — the actor parses it into a structured query, location, lead count, and filter set. Examples: \"find 50 dentists in Dubai with email\", \"plumbers in Karachi Pakistan rating above 4\", \"100 coffee shops in NYC no website\". When provided, the parsed values OVERRIDE `searchTerms`/`location`/`maxResults`/individual filters/`deepScan` unless those are also explicitly set. AI parsing uses the actor's pre-configured Groq API key; a rule-based fallback parser kicks in if it's missing."
                    },
                    "searchTerms": {
                        "title": "Search terms",
                        "type": "array",
                        "description": "One or more search queries to run on Google Maps. Each term is searched independently and results are merged into one dataset. Combined with `placeCategories` if both are set. Ignored when `prompt` is set, or when `placeUrls`/`placeIds` are supplied.",
                        "items": {
                            "type": "string"
                        }
                    },
                    "placeCategories": {
                        "title": "Place categories (multiply with search terms)",
                        "type": "array",
                        "description": "Optional category list — the actor expands each searchTerm × category combination. Example: searchTerms=[\"luxury\"], placeCategories=[\"hotels\",\"restaurants\"] → searches \"luxury hotels\" and \"luxury restaurants\". Leave empty to use search terms verbatim.",
                        "items": {
                            "type": "string"
                        }
                    },
                    "placeUrls": {
                        "title": "Google Maps URLs (alternative to search)",
                        "type": "array",
                        "description": "Direct Maps URLs to scrape. RECOMMENDED FORMAT: `https://www.google.com/maps/place/?q=place_id:<id>` — works reliably. The `/maps/place/<Name>/data=!4m7!...` form that Google's 'Share → Copy link' produces is unreliable from headless browsers (Maps doesn't fully render the detail pane on cold-start). If you have one of those, prefer using `placeIds` below — the actor converts those to the working URL form automatically.",
                        "items": {
                            "type": "string"
                        }
                    },
                    "placeIds": {
                        "title": "Place IDs (alternative to search)",
                        "type": "array",
                        "description": "Google Place IDs (e.g. `ChIJN1t_tDeuEmsRUsoyG83frY4`). Each is converted to a Maps URL and scraped exactly like `placeUrls`. Combine freely with `placeUrls`.",
                        "items": {
                            "type": "string"
                        }
                    },
                    "location": {
                        "title": "Location (free-text)",
                        "type": "string",
                        "description": "City, region, or country to scope the search to. Combined with each search term as \"<term> in <location>\". Leave empty to fall back to the structured Country/City/State fields below."
                    },
                    "country": {
                        "title": "Country",
                        "type": "string",
                        "description": "Used only when free-text Location above is empty. Combined with City/State/County/Postal code into a single location string."
                    },
                    "city": {
                        "title": "City",
                        "type": "string",
                        "description": "Used only when free-text Location is empty."
                    },
                    "state": {
                        "title": "State / Province",
                        "type": "string",
                        "description": "Used only when free-text Location is empty."
                    },
                    "county": {
                        "title": "County",
                        "type": "string",
                        "description": "Used only when free-text Location is empty. Rarely needed but kept for parity with admin hierarchies."
                    },
                    "postalCode": {
                        "title": "Postal / ZIP code",
                        "type": "string",
                        "description": "Used only when free-text Location is empty."
                    },
                    "maxResults": {
                        "title": "Max results per search term",
                        "minimum": 1,
                        "maximum": 500,
                        "type": "integer",
                        "description": "When filters are off, this is the search cap. When filters are active, the actor over-discovers (up to 3×, capped at 25 URLs) and stops scraping the moment enough records pass — so this becomes an output target.",
                        "default": 50
                    },
                    "language": {
                        "title": "Language",
                        "enum": [
                            "en",
                            "es",
                            "fr",
                            "de",
                            "it",
                            "pt",
                            "nl",
                            "pl",
                            "ru",
                            "ja",
                            "ko",
                            "zh-CN",
                            "ar",
                            "hi"
                        ],
                        "type": "string",
                        "description": "Two-letter language code passed to Google Maps via the `hl` parameter. Affects business names, categories, and review content.",
                        "default": "en"
                    },
                    "deepScan": {
                        "title": "Deep scan — visit each business website to harvest contact details",
                        "type": "boolean",
                        "description": "ON by default. The actor visits every listing's website (homepage + a couple of contact-style sub-pages) to harvest emails, phone numbers, social-media links, and named contacts. The best email/phone is also promoted into the top-level `email` / `phone` fields. Adds ~3-8 seconds per listing that has a website.",
                        "default": true
                    },
                    "extractEmails": {
                        "title": "Quick email lookup (used only when Deep scan is OFF)",
                        "type": "boolean",
                        "description": "Lightweight fallback for when Deep scan is disabled: fetches the homepage and standard contact paths over plain HTTP (no browser) looking for an email. Ignored when Deep scan is on, since deep scan is strictly more thorough.",
                        "default": false
                    },
                    "extractWebsiteFallback": {
                        "title": "Try `/contact` if homepage has no email",
                        "type": "boolean",
                        "description": "Only used by Quick email lookup (when Deep scan is OFF). Doubles per-site latency for sites that don't expose an email at all.",
                        "default": true
                    },
                    "verifyEmails": {
                        "title": "Verify discovered emails (MX-record check)",
                        "type": "boolean",
                        "description": "Runs a syntax check + DNS MX-record lookup on every email the actor produces. Adds an `emailValid` boolean to each record. Adds ~50-200 ms per unique domain (cached). Recommended for cold-outreach lists.",
                        "default": false
                    },
                    "reviewsLimit": {
                        "title": "Reviews to extract per place",
                        "minimum": 0,
                        "maximum": 1000,
                        "type": "integer",
                        "description": "0 = skip reviews scraping entirely. Up to ~200 per place is reasonable; higher counts substantially increase per-place runtime (≈1 s per 10 reviews scrolled). Output appears in a `reviews` array on each record.",
                        "default": 0
                    },
                    "reviewsSort": {
                        "title": "Sort reviews by",
                        "enum": [
                            "newest",
                            "mostRelevant",
                            "highestRating",
                            "lowestRating"
                        ],
                        "type": "string",
                        "description": "How Maps orders the reviews list before we read it. `newest` is best for tracking; `mostRelevant` is the Maps default; `highestRating`/`lowestRating` for sentiment slicing.",
                        "default": "newest"
                    },
                    "reviewsSince": {
                        "title": "Only reviews posted after this date (YYYY-MM-DD)",
                        "type": "string",
                        "description": "Optional cutoff. Reviews older than this date are dropped. Maps shows reviews as relative time (\"3 weeks ago\") so absolute dates are estimated; off-by-a-day is possible."
                    },
                    "reviewsKeywords": {
                        "title": "Filter reviews by keywords (any-match, comma-separated)",
                        "type": "string",
                        "description": "If set, only reviews whose text contains at least one of these terms (case-insensitive) are kept. Empty = no keyword filter."
                    },
                    "includeReviewerData": {
                        "title": "Include reviewer profile data with each review",
                        "type": "boolean",
                        "description": "When on, each review carries the reviewer's display name, profile URL, and total-reviews count where Maps exposes them.",
                        "default": true
                    },
                    "imagesLimit": {
                        "title": "Additional images to extract per place",
                        "minimum": 0,
                        "maximum": 200,
                        "type": "integer",
                        "description": "0 = main photo only (free, always included as `mainImageUrl`). Higher numbers open the photo gallery and harvest URLs. Output appears in an `images` array on each record. The first ~30 images load fast; beyond that requires extra scrolling.",
                        "default": 0
                    },
                    "includeImageAuthors": {
                        "title": "Include uploader name with each image",
                        "type": "boolean",
                        "description": "Adds the photo uploader's display name to each image entry where Maps shows it.",
                        "default": false
                    },
                    "noWebsite": {
                        "title": "Filter: only listings WITHOUT a website",
                        "type": "boolean",
                        "description": "Keep only records whose `website` field is empty.",
                        "default": false
                    },
                    "hasWebsite": {
                        "title": "Filter: only listings WITH a website",
                        "type": "boolean",
                        "description": "Mutually exclusive with the previous toggle.",
                        "default": false
                    },
                    "hasPhone": {
                        "title": "Filter: must have a phone number",
                        "type": "boolean",
                        "description": "Drop records with no phone (Google Maps `phone` field, or `deepScanPhones` if Deep scan is on).",
                        "default": false
                    },
                    "hasEmail": {
                        "title": "Filter: must have an email",
                        "type": "boolean",
                        "description": "Drop records with no email. Without Deep scan, only the Quick-email-lookup result is checked.",
                        "default": false
                    },
                    "hasWhatsapp": {
                        "title": "Filter: must have a WhatsApp link",
                        "type": "boolean",
                        "description": "Keep only records whose deep-scanned social links include `wa.me` or `whatsapp.com`. Effectively requires Deep scan.",
                        "default": false
                    },
                    "skipClosedPlaces": {
                        "title": "Filter: skip permanently / temporarily closed places",
                        "type": "boolean",
                        "description": "Drop records flagged with \"Permanently closed\" or \"Temporarily closed\" on Maps.",
                        "default": false
                    },
                    "exactNameMatch": {
                        "title": "Filter: only places whose name contains the search term",
                        "type": "boolean",
                        "description": "Maps's relevance ranking can return loosely-related places. Enable this to drop records whose `name` doesn't contain at least one of the search terms (case-insensitive). No effect when running from `placeUrls`/`placeIds`.",
                        "default": false
                    },
                    "minRating": {
                        "title": "Filter: minimum rating",
                        "minimum": 0,
                        "maximum": 5,
                        "type": "number",
                        "description": "Drop records with rating below this value. Records with no rating at all are also dropped. Leave blank for no minimum."
                    },
                    "maxRating": {
                        "title": "Filter: maximum rating",
                        "minimum": 0,
                        "maximum": 5,
                        "type": "number",
                        "description": "Drop records with rating above this value. Leave blank for no maximum."
                    },
                    "minReviews": {
                        "title": "Filter: minimum review count",
                        "minimum": 0,
                        "type": "integer",
                        "description": "Drop records with fewer reviews than this. Leave blank for no minimum."
                    },
                    "headless": {
                        "title": "Run browser headless",
                        "type": "boolean",
                        "description": "Disable only for debugging on a local run — Apify's servers are headless regardless.",
                        "default": true
                    },
                    "proxy": {
                        "title": "Proxy configuration",
                        "type": "object",
                        "description": "Use Apify Proxy or your own custom proxies. Strongly recommended — Google rate-limits scrapers from any single IP after a few hundred requests. RESIDENTIAL group is safest.",
                        "default": {
                            "useApifyProxy": true
                        }
                    }
                }
            },
            "runsResponseSchema": {
                "type": "object",
                "properties": {
                    "data": {
                        "type": "object",
                        "properties": {
                            "id": {
                                "type": "string"
                            },
                            "actId": {
                                "type": "string"
                            },
                            "userId": {
                                "type": "string"
                            },
                            "startedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "finishedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "status": {
                                "type": "string",
                                "example": "READY"
                            },
                            "meta": {
                                "type": "object",
                                "properties": {
                                    "origin": {
                                        "type": "string",
                                        "example": "API"
                                    },
                                    "userAgent": {
                                        "type": "string"
                                    }
                                }
                            },
                            "stats": {
                                "type": "object",
                                "properties": {
                                    "inputBodyLen": {
                                        "type": "integer",
                                        "example": 2000
                                    },
                                    "rebootCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "restartCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "resurrectCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "computeUnits": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "options": {
                                "type": "object",
                                "properties": {
                                    "build": {
                                        "type": "string",
                                        "example": "latest"
                                    },
                                    "timeoutSecs": {
                                        "type": "integer",
                                        "example": 300
                                    },
                                    "memoryMbytes": {
                                        "type": "integer",
                                        "example": 1024
                                    },
                                    "diskMbytes": {
                                        "type": "integer",
                                        "example": 2048
                                    }
                                }
                            },
                            "buildId": {
                                "type": "string"
                            },
                            "defaultKeyValueStoreId": {
                                "type": "string"
                            },
                            "defaultDatasetId": {
                                "type": "string"
                            },
                            "defaultRequestQueueId": {
                                "type": "string"
                            },
                            "buildNumber": {
                                "type": "string",
                                "example": "1.0.0"
                            },
                            "containerUrl": {
                                "type": "string"
                            },
                            "usage": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "integer",
                                        "example": 1
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "usageTotalUsd": {
                                "type": "number",
                                "example": 0.00005
                            },
                            "usageUsd": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "number",
                                        "example": 0.00005
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
```
