# Google Maps Scraper - User friendly (`data_ops_main/easy-google-maps-scraper`) Actor

- **URL**: https://apify.com/data\_ops\_main/easy-google-maps-scraper.md
- **Developed by:** [yourlocalhost](https://apify.com/data_ops_main) (community)
- **Categories:** E-commerce, Lead generation, MCP servers
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, NaN 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/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

## Easy Google Maps Places Scraper

Scrape Google Maps places — businesses, points of interest, their ratings,
contact details and more — by **search term**, **location**, **Google Maps URL**
or **place ID**. Built to be the easiest Maps scraper to pick up, with the
features other scrapers charge extra for **included for free**.

> First run in 10 seconds: type a search term, type a location, hit **Start**.
> Everything else has sensible defaults.

---

### Why this actor

| Capability | This actor | Typical Maps scrapers |
|---|---|---|
| Beat the ~120-results-per-area limit | ✅ Automatic adaptive tiling | ⚠️ Manual GeoJSON required |
| Many locations in one run | ✅ Built in, one merged dataset | 💰 Paid orchestrator add-on |
| Cross-search de-duplication | ✅ Built in, free | 💰 Often a paid step |
| Incremental / delta runs | ✅ Built in | ❌ Rare |
| Contact enrichment (emails, socials) | ✅ Bundled | 💰 Separate paid actor |
| Interactive results map | ✅ `results-map.html` | ❌ |
| Beginner-safe cost caps | ✅ Hard `maxTotalPlaces` cap | ⚠️ Easy to overspend |

---

### Quick start

1. Open the actor in the Apify Console.
2. Fill **only** the three default fields:
   - **Search terms** — e.g. `coffee shop`
   - **Location** — e.g. `Boulder, Colorado, USA`
   - **Max places per search term** — keep it small (e.g. `50`) for your first run.
3. Click **Start**. Results appear in the **Dataset** tab; an interactive map is
   saved to the **Storage → Key-value store** as `results-map`.

Everything else lives in collapsible **Advanced** sections — open them only when
you need reviews, enrichment, incremental mode or custom areas.

#### Presets

Pick a **Preset** to auto-configure the actor for a common job. A preset only
fills fields you have not set yourself.

| Preset | What it does |
|---|---|
| **Lead generation** | Contact enrichment ON, larger limits — turn places into leads. |
| **Review analysis** | Reviews ON (100/place), balanced limits. |
| **Quick scan** | Tiny, fast, cheap run — listing data only. |

---

### Input

#### Always visible
| Field | Type | Default | Description |
|---|---|---|---|
| `searchTerms` | string[] | — | What to look for, e.g. `["dentist","orthodontist"]`. |
| `location` | string | — | Free-text place. Resolved automatically — no polygon needed. |
| `maxPlacesPerSearch` | integer | `50` | Cap per search term. |

#### Advanced (collapsed by default)
- **Location targeting** — `locations[]` (native multi-location), structured
  `country` / `state` / `city` / `postalCode`, `startUrls[]` (direct Google Maps
  URLs), `placeIds[]`, and `customGeolocation` (optional GeoJSON / circle override).
- **Search limits & language** — `maxTotalPlaces` (hard cost cap), `language`, `regionCode`.
- **Reviews** — `maxReviews`, `reviewsSort`.
- **Contact enrichment** — `scrapeContactDetails`, `maxEnrichmentPagesPerSite`.
- **Incremental / delta mode** — `incrementalMode`, `onlyNewOrChanged`, `stateStoreName`.
- **Personal data & compliance** — `scrapePersonalData` (master opt-in, see below).
- **Performance & browser** — `listingMode` (`auto` / `http` / `browser`),
  `maxConcurrency`, `maxRequestRetries`, `minTileResultsToSubdivide`, `maxTileDepth`.
- **Proxy** — `proxyConfiguration` (defaults to Apify **RESIDENTIAL** proxy).

You can provide **any combination** of `searchTerms` + locations, `startUrls`
and `placeIds` in a single run.

---

### Output

Each dataset item is one place:

```jsonc
{
  "title": "Ozo Coffee Roasters",
  "categoryName": "Coffee shop",
  "categories": ["Coffee shop", "Cafe"],
  "address": "1015 Pearl St, Boulder, CO 80302, USA",
  "street": "1015 Pearl St",
  "city": "Boulder",
  "state": "CO",
  "postalCode": "80302",
  "countryCode": "US",
  "location": { "lat": 40.0176, "lng": -105.2797 },
  "plusCode": "...",
  "phone": "(303) 544-0500",
  "phoneUnformatted": "+13035440500",
  "website": "https://ozocoffee.com/",
  "totalScore": 4.6,
  "reviewsCount": 1234,
  "reviewsDistribution": { "oneStar": 10, "twoStar": 12, "threeStar": 40, "fourStar": 300, "fiveStar": 872 },
  "price": "$$",
  "openingHours": [{ "day": "Monday", "hours": "7 AM–6 PM" }],
  "permanentlyClosed": false,
  "temporarilyClosed": false,
  "placeId": "ChIJ...",
  "cid": "123456789012345678",
  "fid": "0x...:0x...",
  "url": "https://www.google.com/maps?cid=123456789012345678",
  "imageUrl": "https://lh5.googleusercontent.com/...",
  "additionalInfo": { "Service options": [{ "Dine-in": true }, { "Takeaway": true }] },
  "searchString": "coffee shop",
  "locationQuery": "Boulder, Colorado, USA",
  "scrapedAt": "2026-05-21T10:00:00.000Z",
  "changeStatus": "new",
  "emails": [],
  "additionalPhones": [],
  "socialProfiles": {},
  "reviews": []
}
````

- `emails`, `socialProfiles` and review `reviewerName`/`reviewerId` are **only**
  populated when `scrapePersonalData` is enabled (see Compliance).
- `changeStatus` / `changes` are populated in incremental mode.
- A `results-map` HTML file (interactive Leaflet map of every result) is written
  to the run's key-value store.

***

### How adaptive tiling works

Google Maps only returns ~120 results for any single map view. To get **every**
place in a city, this actor:

1. Resolves your location to a bounding box (OpenStreetMap Nominatim).
2. Searches that area as one tile.
3. **Only if** the tile hits the ~120 cap, it splits the tile into 4 quadrants
   and searches each one — recursively, up to `maxTileDepth`.
4. Sparse tiles are never subdivided, so dense city centres get fine-grained
   coverage while quiet suburbs stay cheap.

You never draw a polygon. For full manual control, the advanced
`customGeolocation` field accepts a GeoJSON `Polygon` / `MultiPolygon`, or
`{ "type": "circle", "coordinates": [lng, lat], "radiusKm": 5 }`.

### Incremental / delta mode

Enable `incrementalMode` to remember places between runs (state is stored in a
named key-value store — set `stateStoreName` to reuse it across a monitoring
job). Each run then classifies every place as **new**, **changed** or
**unchanged**, and writes a `change-report` to the key-value store listing new
places, rating changes, status changes and closures. Set `onlyNewOrChanged` to
push just the deltas to the dataset.

### Contact enrichment

Enable `scrapeContactDetails` to visit each place's website and collect emails,
extra phone numbers and social-media profiles (Facebook, Instagram, LinkedIn,
X/Twitter, YouTube, TikTok). This is **bundled at no extra charge**.

***

### Legal, privacy & compliance

**Personal data is OFF by default.** Reviewer names/IDs, scraped emails and
social-media profiles are only collected when you explicitly enable
`scrapePersonalData`. With it off, those fields are omitted even if reviews or
enrichment are enabled.

You are responsible for ensuring your use complies with Google's Terms of
Service and all applicable laws (including the **GDPR** and **CCPA**). Only
scrape personal data when you have a lawful basis to do so, and never use it for
unsolicited bulk contact. This actor is a tool; lawful use is the operator's
responsibility.

***

### Local development

```bash
## 1. Install dependencies
npm install

## 2. Set your test input (already provided at the path below)
##    storage/key_value_stores/default/INPUT.json

## 3. Run locally with the Apify CLI
apify run --purge

## Results: storage/datasets/default/
## Map:     storage/key_value_stores/default/results-map.html
```

Install the Apify CLI with `npm install -g apify-cli` if you do not have it.
A **residential proxy is strongly recommended** — Google blocks datacenter IPs.
Running locally without Apify proxy will likely be blocked; the actor will then
try the browser fallback automatically (`listingMode: "auto"`).

#### Deploy to the Apify platform

```bash
apify login
apify push
```

#### Run via the Apify API

```bash
curl -X POST "https://api.apify.com/v2/acts/YOUR_USERNAME~easy-google-maps-scraper/runs?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
        "searchTerms": ["coffee shop"],
        "location": "Boulder, Colorado, USA",
        "maxPlacesPerSearch": 50
      }'
```

***

### Example inputs

**Multi-location lead generation**

```json
{
  "searchTerms": ["dentist"],
  "locations": ["Austin, Texas, USA", "Round Rock, Texas, USA"],
  "preset": "lead-generation",
  "scrapePersonalData": true
}
```

**Monitor a city for changes**

```json
{
  "searchTerms": ["restaurant"],
  "location": "Camden, London, UK",
  "incrementalMode": true,
  "onlyNewOrChanged": true,
  "stateStoreName": "camden-restaurants"
}
```

**Scrape specific places by ID**

```json
{
  "placeIds": ["ChIJ4z2Gx...", "123456789012345678"]
}
```

***

### Listing modes (how results are fetched)

Google Maps loads its result list through an internal endpoint whose request
format it rotates frequently. The actor therefore supports three modes via the
advanced `listingMode` field:

| Mode | How it works | Notes |
|---|---|---|
| `auto` *(default)* | Tries the cheap HTTP endpoint first, automatically falls back to a headless browser if HTTP returns nothing. | **Recommended.** Always produces results. |
| `http` | HTTP endpoint only. | Fastest/cheapest **when it works**, but depends on Google's current internal format — treat as experimental. |
| `browser` | Headless Chromium only — drives the real Google Maps UI. | Most robust. This is what `auto` falls back to. |

In `auto` / `browser` mode each place carries the core fields (name, category,
address, rating, coordinates, place IDs, Maps URL). Fields that only exist on
the place's own page — `phone`, `website`, `openingHours` — are **not** filled
by the browser listing path; such records have `partialData: true`. The HTTP
path's parser (`src/parsers.js`) extracts the full field set, so once the HTTP
format is current again every field is populated automatically.

### Pricing notes

- `auto` makes one cheap HTTP attempt per area, then uses a headless browser
  for the actual listing. The browser is reused across tiles, not per place.
- `maxTotalPlaces` is a hard ceiling on the whole run — set it to control spend.
- Contact enrichment adds one (or a few) lightweight HTTP requests per website.
- Proxy traffic (residential, recommended for Maps) is billed by your Apify plan.

### Limitations & troubleshooting

- **Zero results / "Blocked by Google"** — use Apify **RESIDENTIAL** proxy.
  `auto` mode already retries with a browser; persistent blocking is almost
  always a proxy-quality issue.
- `partialData: true` means the record came from the browser listing path and
  lacks `phone` / `website` / `openingHours` (see *Listing modes* above).
- Google Maps' internal data format is undocumented and changes occasionally.
  Every field is parsed defensively, so a format change degrades individual
  fields rather than crashing the run. If many HTTP-path fields come back empty,
  the array indices in `src/parsers.js` (`parsePlaceArray`) are the place to update.
- **Reviews** extraction returns the batch embedded in the place page
  (best-effort). For deep, paginated review scraping use a dedicated reviews actor.
- Results are held in memory until the run finishes (bounded by `maxTotalPlaces`).

# Actor input Schema

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

What to look for on Google Maps, e.g. 'coffee shop', 'dentist', 'hotel'. Add as many as you like — each one is searched across every location.

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

Free-text place to search in, e.g. 'San Francisco, California, USA' or 'Camden, London'. Resolved automatically to a map area — you never need to draw a polygon.

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

Upper bound on results returned for each search term (across all locations and map tiles). Keep this modest for your first run so it stays cheap.

## `preset` (type: `string`):

Optional one-click configuration. A preset only fills fields you have left at their default — anything you set yourself always wins.

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

Search several locations in one run and get a single merged, de-duplicated dataset. No paid orchestrator needed. Used in addition to the single 'Location' field above.

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

Structured location part. Combined with state/city/postal code into a single location query.

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

Structured location part.

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

Structured location part.

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

Structured location part.

## `startUrls` (type: `array`):

Direct Google Maps URLs — a search results URL, a place URL, or a shortened goo.gl/maps link. Scraped as-is in addition to any search terms.

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

Scrape specific places directly by Google place ID (ChIJ...) or numeric CID. One per line.

## `customGeolocation` (type: `object`):

Optional GeoJSON Polygon/MultiPolygon, or an object {"type":"circle","coordinates":\[lng,lat],"radiusKm":5}. Overrides automatic location resolution. Leave empty to let the actor resolve and tile the area for you.

## `maxTotalPlaces` (type: `integer`):

Absolute ceiling on dataset size for the whole run, no matter how many search terms or locations are supplied. Protects you from a runaway bill.

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

Two-letter language code used for Google Maps content (hl parameter).

## `regionCode` (type: `string`):

Two-letter country code used to bias results (gl parameter), e.g. 'us', 'gb', 'de'.

## `maxReviews` (type: `integer`):

Reviews to scrape per place. 0 = skip reviews entirely (fastest, cheapest). Reviews contain reviewer names, which are personal data — see the compliance section below.

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

Order in which reviews are fetched.

## `scrapeContactDetails` (type: `boolean`):

Visit each place's website and extract emails, extra phone numbers and social media profiles (Facebook, Instagram, LinkedIn, X/Twitter, YouTube, TikTok). Bundled at no extra charge. Emails and social profiles can be personal data — requires the personal-data opt-in below.

## `maxEnrichmentPagesPerSite` (type: `integer`):

When enriching, how many pages of each website to scan (home page plus likely 'contact' pages).

## `incrementalMode` (type: `boolean`):

Remember which places were seen on previous runs (stored in a named key-value store). Lets the actor detect new, changed and closed places between runs.

## `onlyNewOrChanged` (type: `boolean`):

When incremental mode is on, push only places that are new or whose key fields changed since the last run. Unchanged places are skipped.

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

Name of the named key-value store used to persist incremental state. Use the same name across runs of the same monitoring job. Leave empty to auto-name it per actor.

## `scrapePersonalData` (type: `boolean`):

Master opt-in for personal data: reviewer names/IDs, scraped emails and social profiles. When OFF, those fields are omitted even if reviews/enrichment are enabled. You are responsible for compliance with Google's Terms and applicable law (GDPR/CCPA). See the README.

## `listingMode` (type: `string`):

'auto' (recommended) tries a cheap HTTP request first and automatically falls back to a headless browser if HTTP returns nothing. 'http' forces HTTP only — fastest, but depends on Google's frequently-rotated internal format, so treat it as experimental. 'browser' forces the headless Chromium path, which is the most robust.

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

Maximum parallel requests.

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

How many times to retry a blocked or failed request before giving up on it.

## `minTileResultsToSubdivide` (type: `integer`):

A map tile is split into 4 sub-tiles when it returns at least this many results (i.e. it likely hit Google's per-view cap). Lower = more thorough but more requests.

## `maxTileDepth` (type: `integer`):

How many times a tile may be recursively quartered. Each level multiplies coverage and cost. 4 is plenty for a city.

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

Google Maps blocks datacenter IPs aggressively — RESIDENTIAL proxy is strongly recommended and is the default.

## Actor input object example

```json
{
  "searchTerms": [
    "coffee shop"
  ],
  "location": "Boulder, Colorado, USA",
  "maxPlacesPerSearch": 50,
  "preset": "none",
  "locations": [],
  "startUrls": [],
  "placeIds": [],
  "maxTotalPlaces": 1000,
  "language": "en",
  "regionCode": "us",
  "maxReviews": 0,
  "reviewsSort": "newest",
  "scrapeContactDetails": false,
  "maxEnrichmentPagesPerSite": 3,
  "incrementalMode": false,
  "onlyNewOrChanged": false,
  "stateStoreName": "",
  "scrapePersonalData": false,
  "listingMode": "auto",
  "maxConcurrency": 10,
  "maxRequestRetries": 5,
  "minTileResultsToSubdivide": 100,
  "maxTileDepth": 4,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "searchTerms": [
        "coffee shop"
    ],
    "location": "Boulder, Colorado, USA",
    "maxPlacesPerSearch": 50,
    "locations": [],
    "startUrls": [],
    "placeIds": [],
    "maxTotalPlaces": 1000,
    "maxReviews": 0,
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("data_ops_main/easy-google-maps-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 = {
    "searchTerms": ["coffee shop"],
    "location": "Boulder, Colorado, USA",
    "maxPlacesPerSearch": 50,
    "locations": [],
    "startUrls": [],
    "placeIds": [],
    "maxTotalPlaces": 1000,
    "maxReviews": 0,
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("data_ops_main/easy-google-maps-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 '{
  "searchTerms": [
    "coffee shop"
  ],
  "location": "Boulder, Colorado, USA",
  "maxPlacesPerSearch": 50,
  "locations": [],
  "startUrls": [],
  "placeIds": [],
  "maxTotalPlaces": 1000,
  "maxReviews": 0,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call data_ops_main/easy-google-maps-scraper --silent --output-dataset

```

## MCP server setup

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

```

## OpenAPI specification

```json
{
    "openapi": "3.0.1",
    "info": {
        "title": "Google Maps Scraper - User friendly",
        "version": "0.1",
        "x-build-id": "Hb7PFb72XHgGmEees"
    },
    "servers": [
        {
            "url": "https://api.apify.com/v2"
        }
    ],
    "paths": {
        "/acts/data_ops_main~easy-google-maps-scraper/run-sync-get-dataset-items": {
            "post": {
                "operationId": "run-sync-get-dataset-items-data_ops_main-easy-google-maps-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/data_ops_main~easy-google-maps-scraper/runs": {
            "post": {
                "operationId": "runs-sync-data_ops_main-easy-google-maps-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/data_ops_main~easy-google-maps-scraper/run-sync": {
            "post": {
                "operationId": "run-sync-data_ops_main-easy-google-maps-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": {
                    "searchTerms": {
                        "title": "Search terms",
                        "type": "array",
                        "description": "What to look for on Google Maps, e.g. 'coffee shop', 'dentist', 'hotel'. Add as many as you like — each one is searched across every location.",
                        "default": [],
                        "items": {
                            "type": "string"
                        }
                    },
                    "location": {
                        "title": "Location",
                        "type": "string",
                        "description": "Free-text place to search in, e.g. 'San Francisco, California, USA' or 'Camden, London'. Resolved automatically to a map area — you never need to draw a polygon."
                    },
                    "maxPlacesPerSearch": {
                        "title": "Max places per search term",
                        "minimum": 1,
                        "maximum": 100000,
                        "type": "integer",
                        "description": "Upper bound on results returned for each search term (across all locations and map tiles). Keep this modest for your first run so it stays cheap.",
                        "default": 50
                    },
                    "preset": {
                        "title": "Preset",
                        "enum": [
                            "none",
                            "lead-generation",
                            "review-analysis",
                            "quick-scan"
                        ],
                        "type": "string",
                        "description": "Optional one-click configuration. A preset only fills fields you have left at their default — anything you set yourself always wins.",
                        "default": "none"
                    },
                    "locations": {
                        "title": "Multiple locations",
                        "type": "array",
                        "description": "Search several locations in one run and get a single merged, de-duplicated dataset. No paid orchestrator needed. Used in addition to the single 'Location' field above.",
                        "default": [],
                        "items": {
                            "type": "string"
                        }
                    },
                    "country": {
                        "title": "Country",
                        "type": "string",
                        "description": "Structured location part. Combined with state/city/postal code into a single location query."
                    },
                    "state": {
                        "title": "State / region",
                        "type": "string",
                        "description": "Structured location part."
                    },
                    "city": {
                        "title": "City",
                        "type": "string",
                        "description": "Structured location part."
                    },
                    "postalCode": {
                        "title": "Postal / ZIP code",
                        "type": "string",
                        "description": "Structured location part."
                    },
                    "startUrls": {
                        "title": "Google Maps URLs",
                        "type": "array",
                        "description": "Direct Google Maps URLs — a search results URL, a place URL, or a shortened goo.gl/maps link. Scraped as-is in addition to any search terms.",
                        "default": [],
                        "items": {
                            "type": "string"
                        }
                    },
                    "placeIds": {
                        "title": "Place IDs / CIDs",
                        "type": "array",
                        "description": "Scrape specific places directly by Google place ID (ChIJ...) or numeric CID. One per line.",
                        "default": [],
                        "items": {
                            "type": "string"
                        }
                    },
                    "customGeolocation": {
                        "title": "Custom geolocation (advanced override)",
                        "type": "object",
                        "description": "Optional GeoJSON Polygon/MultiPolygon, or an object {\"type\":\"circle\",\"coordinates\":[lng,lat],\"radiusKm\":5}. Overrides automatic location resolution. Leave empty to let the actor resolve and tile the area for you."
                    },
                    "maxTotalPlaces": {
                        "title": "Max total places (hard cap)",
                        "minimum": 1,
                        "maximum": 100000,
                        "type": "integer",
                        "description": "Absolute ceiling on dataset size for the whole run, no matter how many search terms or locations are supplied. Protects you from a runaway bill.",
                        "default": 1000
                    },
                    "language": {
                        "title": "Results language",
                        "type": "string",
                        "description": "Two-letter language code used for Google Maps content (hl parameter).",
                        "default": "en"
                    },
                    "regionCode": {
                        "title": "Region / country bias",
                        "type": "string",
                        "description": "Two-letter country code used to bias results (gl parameter), e.g. 'us', 'gb', 'de'.",
                        "default": "us"
                    },
                    "maxReviews": {
                        "title": "Max reviews per place",
                        "minimum": 0,
                        "maximum": 5000,
                        "type": "integer",
                        "description": "Reviews to scrape per place. 0 = skip reviews entirely (fastest, cheapest). Reviews contain reviewer names, which are personal data — see the compliance section below.",
                        "default": 0
                    },
                    "reviewsSort": {
                        "title": "Reviews sort order",
                        "enum": [
                            "newest",
                            "mostRelevant",
                            "highestRanking",
                            "lowestRanking"
                        ],
                        "type": "string",
                        "description": "Order in which reviews are fetched.",
                        "default": "newest"
                    },
                    "scrapeContactDetails": {
                        "title": "Enrich with contact details",
                        "type": "boolean",
                        "description": "Visit each place's website and extract emails, extra phone numbers and social media profiles (Facebook, Instagram, LinkedIn, X/Twitter, YouTube, TikTok). Bundled at no extra charge. Emails and social profiles can be personal data — requires the personal-data opt-in below.",
                        "default": false
                    },
                    "maxEnrichmentPagesPerSite": {
                        "title": "Max pages to scan per website",
                        "minimum": 1,
                        "maximum": 20,
                        "type": "integer",
                        "description": "When enriching, how many pages of each website to scan (home page plus likely 'contact' pages).",
                        "default": 3
                    },
                    "incrementalMode": {
                        "title": "Incremental / delta mode",
                        "type": "boolean",
                        "description": "Remember which places were seen on previous runs (stored in a named key-value store). Lets the actor detect new, changed and closed places between runs.",
                        "default": false
                    },
                    "onlyNewOrChanged": {
                        "title": "Output only new or changed places",
                        "type": "boolean",
                        "description": "When incremental mode is on, push only places that are new or whose key fields changed since the last run. Unchanged places are skipped.",
                        "default": false
                    },
                    "stateStoreName": {
                        "title": "State store name",
                        "type": "string",
                        "description": "Name of the named key-value store used to persist incremental state. Use the same name across runs of the same monitoring job. Leave empty to auto-name it per actor.",
                        "default": ""
                    },
                    "scrapePersonalData": {
                        "title": "I have a lawful basis to scrape personal data",
                        "type": "boolean",
                        "description": "Master opt-in for personal data: reviewer names/IDs, scraped emails and social profiles. When OFF, those fields are omitted even if reviews/enrichment are enabled. You are responsible for compliance with Google's Terms and applicable law (GDPR/CCPA). See the README.",
                        "default": false
                    },
                    "listingMode": {
                        "title": "Listing mode",
                        "enum": [
                            "auto",
                            "http",
                            "browser"
                        ],
                        "type": "string",
                        "description": "'auto' (recommended) tries a cheap HTTP request first and automatically falls back to a headless browser if HTTP returns nothing. 'http' forces HTTP only — fastest, but depends on Google's frequently-rotated internal format, so treat it as experimental. 'browser' forces the headless Chromium path, which is the most robust.",
                        "default": "auto"
                    },
                    "maxConcurrency": {
                        "title": "Max concurrency",
                        "minimum": 1,
                        "maximum": 100,
                        "type": "integer",
                        "description": "Maximum parallel requests.",
                        "default": 10
                    },
                    "maxRequestRetries": {
                        "title": "Max request retries",
                        "minimum": 0,
                        "maximum": 20,
                        "type": "integer",
                        "description": "How many times to retry a blocked or failed request before giving up on it.",
                        "default": 5
                    },
                    "minTileResultsToSubdivide": {
                        "title": "Tile subdivision threshold",
                        "minimum": 20,
                        "maximum": 120,
                        "type": "integer",
                        "description": "A map tile is split into 4 sub-tiles when it returns at least this many results (i.e. it likely hit Google's per-view cap). Lower = more thorough but more requests.",
                        "default": 100
                    },
                    "maxTileDepth": {
                        "title": "Max tile subdivision depth",
                        "minimum": 0,
                        "maximum": 7,
                        "type": "integer",
                        "description": "How many times a tile may be recursively quartered. Each level multiplies coverage and cost. 4 is plenty for a city.",
                        "default": 4
                    },
                    "proxyConfiguration": {
                        "title": "Proxy configuration",
                        "type": "object",
                        "description": "Google Maps blocks datacenter IPs aggressively — RESIDENTIAL proxy is strongly recommended and is the default.",
                        "default": {
                            "useApifyProxy": true,
                            "apifyProxyGroups": [
                                "RESIDENTIAL"
                            ]
                        }
                    }
                }
            },
            "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
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
```
