# Google Maps Places Scraper (`smart_albatross/google-maps-places-scraper`) Actor

Every Google Maps place for a search and area in one run: contacts, reviews breakdown, popular times, hours, photos and 50+ more fields.

- **URL**: https://apify.com/smart_albatross/google-maps-places-scraper.md
- **Developed by:** [Dev](https://apify.com/smart_albatross) (community)
- **Stats:** 3 total users, 2 monthly users, 95.7% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 1,000 place scrapeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Google Maps Places Scraper

Get every Google Maps place for a search and an area in **one run**, with **81 fields per place**: contacts from the business's own website, the full 1–5 star review breakdown, review keywords, popular times, live busyness, 7-day opening hours and whether it is open right now, Google's review summary, the menu or services list with prices, owner, plus code, photos, menu and booking links, and a hotel block. No second actor, no API key, no chaining.

Built for lead lists, market mapping, local-SEO audits and location research, wherever you need the **whole** row rather than a name and a phone number.

### Why this one

- **One run, one row, all fields.** Search, place details and website contacts are fetched in the same run. You never have to feed one scraper's output into another.
- **Exact review counts.** The review count is the sum of Google's own star histogram, so `reviews` and `reviewBreakdown` always agree. On a measured 500-place run, the histogram's average matched the displayed rating to within 0.1 on every place.
- **Stays inside the area you asked for.** Cities are searched inside their real boundary (a "New York City" search does not return Jersey City). Results are gathered outward from the point you searched.
- **Fast and light.** 500 places, with full details and website contacts, in 27–37 seconds with a residential proxy pool (about 50 seconds on the smallest datacenter pool). Most of the tail is waiting on slow business websites.
- **Tracks changes between runs.** Name a search with `monitoringKey` and every place comes back marked `new`, `changed` or `unchanged`, with the exact changes ("rating 4.2 → 3.9", "now closed", "phone changed"). Add `onlyChanges` and scheduled re-runs return — and charge for — only what changed.
- **Clean contacts, free.** Phones normalised to international format with their line type (mobile / landline). Every email's domain checked for a mail server, and the business's own-domain addresses ranked first.
- **Machine-readable.** Opening hours as open/close times per day and price ranges as numbers with a currency — no parsing on your side.
- **Fails loudly.** A run that finds nothing ends as *Failed* with the reason ("the location could not be found", "all search requests were blocked", "your filters removed every place"). It never ends as a successful empty dataset.

### Quick start

Search a category in a city:

```json
{ "searchTerms": ["dentist"], "location": "Jaipur, India", "maxPlacesPerSearch": 500 }
```

Search a precise area (a circle or a box):

```json
{ "searchTerms": ["coffee shop"], "customArea": { "lat": 40.7549, "lng": -73.9840, "radiusKm": 2 } }
```

Look up specific places (URLs, `maps.app.goo.gl` short links, or place IDs):

```json
{ "startUrls": [{ "url": "https://maps.app.goo.gl/…" }], "placeIds": ["ChIJNV5sGiH8mzkR_hUMnJI9RHU"] }
```

### Input

| Field | Default | What it does |
|---|---|---|
| `searchTerms` | — | What to search for. Each term is searched over the whole area. |
| `location` | — | City, neighbourhood or region, resolved with OpenStreetMap. |
| `customArea` | — | `{lat, lng, radiusKm}` or `{south, north, west, east}`. Overrides `location`. |
| `startUrls` | — | Google Maps search URLs (`…/maps/search/pizza/@lat,lng,14z`) or place URLs. |
| `placeIds` | — | Place IDs (`ChIJ…`) to fetch directly. |
| `maxPlacesPerSearch` | 500 | Cap per search term. |
| `maxPlaces` | 0 (no cap) | Cap for the whole run. Exact: you are never charged past it. |
| `coverage` | `balanced` | `fast` / `balanced` / `exhaustive`: how finely the area is scanned (see below). |
| `scrapeContacts` | `true` | Read each business website for emails, extra phones, social and directory profiles. |
| `language` | `en` | Language of categories, hours and labels. |
| `countryCode` | from location | Two-letter market code. |
| `minRating` | — | Only places rated at least this (unrated places are excluded). |
| `website` | `all` | `all` / `withWebsite` / `withoutWebsite`. |
| `skipClosed` | `false` | Leave out places Google marks as closed. |
| `maxImages` | 10 | Photo URLs per place. |
| `verifyEmails` | `true` | Check each email domain can receive mail; rank own-domain addresses first. Free. |
| `monitoringKey` | — | Name a search to track changes between runs (see below). |
| `onlyChanges` | `false` | With `monitoringKey`: return and charge only new or changed places. |
| `proxyConfiguration` | Apify Proxy | Proxy for Google requests. |

### Output

One item per place. Every key is always present: empty lists are `[]`, and unknown values are `null`, never a made-up zero.

| Field | Meaning |
|---|---|
| `placeUrl`, `placeId` | Google Maps link and place ID |
| `businessName`, `category`, `allCategories`, `categoryId` | Name, main category, all categories, and a language-independent category slug |
| `placeTags` | Identity and inclusivity attributes the business declares ("Identifies as women-owned", "LGBTQ+ friendly") |
| `rating`, `reviews`, `reviewBreakdown` | Star rating, review count, and the 1–5 star histogram |
| `reviewKeywords` | The topics reviews mention most, with counts |
| `reviewsUrl` | Google's reviews page for the place |
| `priceRange` | Google's price band, e.g. `$10–20` or `₹200–800` |
| `phone`, `website` | Main phone (international format) and website |
| `emails`, `additionalPhones`, `socials`, `directoryProfiles` | From the business's own website: emails, other phones, Facebook/Instagram/X/LinkedIn/YouTube/TikTok/Pinterest, and Yelp/Tripadvisor/Foursquare pages |
| `address`, `street`, `neighborhood`, `city`, `state`, `postalCode`, `country` | Address, as Google's own labelled components |
| `latitude`, `longitude`, `entranceLocation` | Map pin and Google's navigation arrival point |
| `plusCode`, `locatedIn`, `floor` | Plus Code; the venue it is inside (e.g. a mall); floor, when the address states one |
| `permanentlyClosed`, `temporarilyClosed`, `unclaimed` | Status flags |
| `owner` | Business profile owner `{name, id, url}` |
| `description`, `attributes` | The business's description; Google's attribute groups (Service options, Accessibility, Payments, …; for hotels, the amenities: Wi-Fi, Pools, Parking, …) |
| `openingHours`, `specialHours` | All seven days; per-service hours (e.g. drive-through, delivery) |
| `popularTimes`, `liveOccupancyPercent`, `liveStatus` | Hourly busyness for each weekday; live busyness (only when Google has a live reading) |
| `coverPhoto`, `photos`, `photoCount` | Real photos (Street View frames are kept apart in `streetViewImage`) |
| `relatedPlaces` | Places at the same address (e.g. a mall's tenants) |
| `ownerPosts` | The business's own updates |
| `menuUrl`, `reserveTableUrl`, `reservationLinks` | Menu and booking links |
| `hotelType`, `hotelDescription`, `similarHotels`, `hotelAds` | Hotel class, description, comparable hotels nearby, and live booking offers |
| `editorialSummary`, `reviewsSummary` | Google's one-line summary of the place, and its summary of what reviewers say |
| `offerings` | What the business lists: menu items, services or products, with section, description, price and photo |
| `openNow`, `currentStatus`, `hoursConfirmed` | Open at the moment of the run (in the place's own time zone), e.g. `Open · closes 18:00`; and Google's "Confirmed by this business …" note |
| `nameLocal` | The name in the local script, when Google has one (e.g. `थे रॉयल हर्मिटेज`) |
| `openingHoursStructured` | Hours as data: `{"Monday": [{"open": "06:30", "close": "18:00"}], "Sunday": []}` |
| `priceMin`, `priceMax`, `priceCurrency` | The price range as numbers: `"₹200–800"` → `200`, `800`, `INR` |
| `phoneE164`, `phoneType` | The main phone in international format, and `mobile` / `landline` / `toll_free` … |
| `emailDetails` | Per email: `onWebsiteDomain`, `freeProvider`, `role` (info@, sales@ …) and `domainAcceptsMail` |
| `visitDuration` | How long people typically stay, e.g. `People typically spend 1-3.5 hours here` |
| `reviewHighlights` | The review quotes Google features, with the reviewer's profile link |
| `evCharging` | EV charging stations at the place, with connector types and speeds |
| `storeDirectory` | For malls and venues: shop categories and how many places each holds |
| `photoDetails` | Photos with caption, dish, uploader, upload date, size, and whether the owner posted it |
| `changeType`, `changes` | With `monitoringKey`: `new` / `changed` / `unchanged` and `[{field, from, to}]` |
| `searchString`, `rank`, `scrapedAt`, `streetViewImage`, `coverage` | Run context. `coverage` says whether details and contacts were fetched and where the review count came from. |

A real (trimmed) item:

```json
{
  "businessName": "Om Dental Clinic",
  "placeUrl": "https://www.google.com/maps/place/?q=place_id:ChIJDy5wQEGxbTkRoIr-g0ZK874",
  "category": "Medical clinic",
  "allCategories": ["Medical clinic", "Dental clinic", "Dentist", "Orthodontist"],
  "rating": 5, "reviews": 26,
  "reviewBreakdown": { "oneStar": 0, "twoStar": 0, "threeStar": 0, "fourStar": 0, "fiveStar": 26 },
  "reviewKeywords": [{ "keyword": "dental care", "count": 4 }, { "keyword": "friendly doctor", "count": 4 }],
  "phone": "+91 90011 78656",
  "address": "Plot no.10, Gurunanak Colony, Tripolia Bazar, Jaipur, Rajasthan 302002",
  "postalCode": "302002", "country": "IN", "latitude": 26.9348028, "longitude": 75.8237201,
  "plusCode": "WRMF+WF Jaipur, Rajasthan",
  "openingHours": { "Monday": "10 am–2 pm", "Tuesday": "10 am–2 pm", "Sunday": "10 am–2 pm" },
  "photoCount": 12,
  "coverage": { "detail": "ok", "contacts": "ok", "reviewCountSource": "histogram" }
}
```

### Pricing

Pay per event. You pay only for places actually delivered:

| Event | Price | When |
|---|---|---|
| Place scraped | $0.0015 | Each place row, including all Google Maps details |
| Contacts scraped | $0.001 | Only when the place has a website **and** it could actually be read |

Example: 1,000 places, 600 of them with a readable website, costs 1,000 × $0.0015 + 600 × $0.001 = **$2.10**. Duplicates, and places removed by your filters, are never charged. Set a maximum charge on the run and it stops cleanly when that is reached.

### Tracking changes between runs

Give a search a name and schedule it:

```json
{ "searchTerms": ["dentist"], "location": "Jaipur, India", "monitoringKey": "jaipur-dentists", "onlyChanges": true }
```

The first run returns everything as `new`. Each later run compares with the one before and returns only places that are new or whose name, category, rating, review count, phone, website, address, price range, hours or closed status changed — each with `changes: [{"field": "rating", "from": 4.2, "to": 3.9}]`. Unchanged places are not charged. The run summary (`SUMMARY` in the key-value store) also lists places the previous run found and this one did not. Snapshots are kept in your account's `places-monitor` key-value store.

### How coverage works

Google Maps answers a search for one map view at a time, and favours places near the middle of that view. To cover a whole city, the area is split into a grid of views, searched from the centre outwards. Any view that is still full of new places is split into four smaller, closer views. `coverage` sets how fine the starting grid is:

- `fast`: large views, the main results, the fewest requests.
- `balanced` (default): a good compromise for neighbourhoods and cities.
- `exhaustive`: small views, finds the long tail of small businesses in a big area, and takes longer.

When an area is too large for the chosen coverage, each row's `coverage.coarsened` is `true` and the log says so. Use `exhaustive`, or a smaller area, for more complete results.

### Known limits

Stated plainly, so nothing surprises you:

- **`temporarilyClosed`** is always `false`. Google's closed flag does not say whether a closure is temporary, so closed places show `permanentlyClosed: true`.
- **`skipClosed`** filters search results. Places you look up by URL or ID are always returned, with `permanentlyClosed` telling you their status.
- **`relatedPlaces`** lists places *at the same address* (tenants of a mall, a hotel's restaurant), not Google's "People also search for".
- **`floor`** is filled only when the business writes its floor into its address.
- **`liveStatus` / `liveOccupancyPercent` and `hotelAds`** are live data. They appear only when Google has a live reading or live offers at the moment of the run.
- **`photoCount`** is empty, not zero, for places with no photos.
- **`emails`** come from the business's own site. On sites built with a shared website builder, they can include the builder's addresses.
- **Review text is not included.** You get the count, the star breakdown and the review keywords.

### FAQ

**The run failed. Why?** The status message says why: an unknown location, requests blocked by Google, a term that names a single place outside your area, or filters that removed everything. A failed run with no results is never charged for places.

**Do I need a proxy?** The default Apify Proxy works. Google serves fuller place data through the proxy than from shared platform IPs, so keep it on.

**Can I stop at N places?** Yes. `maxPlaces` is exact, and the run also respects the maximum charge you set.

# Actor input Schema

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

What to search for, e.g. "dentist" or "coffee shop". Each term is searched over the whole area.

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

City, neighbourhood or region, e.g. "Jaipur, India". Resolved with OpenStreetMap.

## `customArea` (type: `object`):

Instead of a location: {"lat": 40.75, "lng": -73.98, "radiusKm": 2} or {"south": .., "north": .., "west": .., "east": ..}.

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

Search URLs (…/maps/search/pizza/@lat,lng,14z) or place URLs, including maps.app.goo.gl short links.

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

Google place IDs (ChIJ…) to fetch directly.

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

Stop each search term after this many places.

## `maxPlaces` (type: `integer`):

Stop the whole run after this many places. 0 = no limit.

## `coverage` (type: `string`):

How finely the area is scanned. Finer finds more small businesses but takes longer.

## `scrapeContacts` (type: `boolean`):

Visit each business website for emails, extra phones, social and directory profiles. Billed per place with a website that could be read.

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

Check that each email's domain can receive mail (MX record) and rank the business's own-domain addresses first. Free.

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

Language of category names, hours and labels, e.g. en, de, pt-BR.

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

Two-letter market code (us, in, gb…). Detected from the location when empty.

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

Only places rated at least this. Unrated places are excluded when set.

## `website` (type: `string`):

Keep only places with, or only places without, a website.

## `skipClosed` (type: `boolean`):

Leave out places Google marks as closed.

## `maxImages` (type: `integer`):

How many photo URLs to include per place (0 = none).

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

Proxy for Google requests.

## `monitoringKey` (type: `string`):

Give the search a name (e.g. "jaipur-dentists") to compare every run with the previous run under the same name. Each place then gets changeType (new / changed / unchanged) and the exact field changes.

## `onlyChanges` (type: `boolean`):

With a monitoringKey: unchanged places are left out and not charged. Ideal for scheduled runs.

## Actor input object example

```json
{
  "searchTerms": [
    "coffee shop"
  ],
  "location": "New York, USA",
  "maxPlacesPerSearch": 500,
  "maxPlaces": 0,
  "coverage": "balanced",
  "scrapeContacts": true,
  "verifyEmails": true,
  "language": "en",
  "website": "all",
  "skipClosed": false,
  "maxImages": 10,
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "onlyChanges": false
}
```

# Actor output Schema

## `places` (type: `string`):

Every place found, one item each with all fields.

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

Counts for the run (places found, filtered, failed), per-field fill rates and, with change tracking, how many places were new, changed or unchanged.

# 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": "New York, USA",
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("smart_albatross/google-maps-places-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": "New York, USA",
    "proxyConfiguration": { "useApifyProxy": True },
}

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

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

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

```

## CLI example

```bash
echo '{
  "searchTerms": [
    "coffee shop"
  ],
  "location": "New York, USA",
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call smart_albatross/google-maps-places-scraper --silent --output-dataset

```

## MCP server setup

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

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/hUpRll98rbnDdDSPB/builds/xqq4zfVh8FsvMfAb9/openapi.json
