# Google Maps Scraper — Places, Reviews, Phones & Hours (`tortuga/google-maps-scraper`) Actor

Scrape Google Maps places by keyword and location: name, address, phone, website, rating, review count, hours and coordinates, plus optional details (popular times, amenities) and reviews.

- **URL**: https://apify.com/tortuga/google-maps-scraper.md
- **Developed by:** [Trevor Ortega](https://apify.com/tortuga) (community)
- **Categories:** Lead generation, Travel
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.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 Scraper — Places, Reviews, Phones & Hours

Scrape Google Maps like a lead list: search any keyword in any city, ZIP code or coordinates and get every place
with its **name, phone, website, full address, coordinates, rating, review count, price level, opening hours and
open/closed status**. Turn on details for the **rating histogram, popular times, "About" attributes, menu,
reservation and order links, photos and owner info**, and add **reviews** (rating, text, reviewer, Local Guide
flag, photos, likes, owner response).

- No Google API key, no login, no browser: the actor sends Google Maps' own JSON requests through Apify's
  datacenter proxy.
- Pay per result: places, details and reviews are separate events, so you only pay for what you turn on.
- Place IDs, CIDs, Google Maps place/search URLs and maps.app.goo.gl short links work as input too.
- Built-in change monitoring for scheduled runs: only new places, changed ratings/review counts/closures and new
  reviews are output and charged.

### What data does Google Maps Scraper extract?

Every place (`"type": "place"`):

| Field | Description |
|---|---|
| `placeId`, `cid`, `featureId`, `kgmid` | Google place ID (`ChIJ...`), CID (the number in `maps?cid=`), Maps feature id and Knowledge Graph id |
| `name`, `category`, `categories` | Business name and all Google categories |
| `address`, `street`, `neighborhood`, `city`, `postalCode`, `state`, `countryCode` | Full address line and its parts |
| `latitude`, `longitude`, `plusCode`, `timezone` | Location |
| `phone`, `phoneInternational` | Phone as Google shows it and in E.164 format (`+15124699003`) |
| `website` | Business website |
| `rating`, `reviewCount`, `priceLevel` | Stars, number of reviews, price level / "price per person" range (`$10–20`) when Google shows one |
| `openingHours`, `openStatus` | Weekly hours `[{"day": "Monday", "hours": "7 AM–6 PM"}, ...]` and the current status line ("Open · Closes 6 PM") |
| `permanentlyClosed`, `temporarilyClosed` | Closure flags (from Google's status line) |
| `isClaimed` | `false` when Google shows "Claim this business" |
| `summary`, `photoUrl` | Google's one-line editorial summary and main photo |
| `googleMapsUrl`, `reviewsUrl` | Links to the place and its reviews |
| `searchTerm`, `searchLocation`, `searchQuery`, `rank`, `startUrl`, `scrapedAt` | Provenance; `rank` = position in its search |

With **Include place details** (`"detailsScraped": true`):

| Field | Description |
|---|---|
| `ratingHistogram` | Number of 1-5 star reviews: `{"1": 64, "2": 45, "3": 94, "4": 328, "5": 878}` |
| `popularTimes`, `popularTimesLiveText`, `popularTimesLivePercent`, `typicalTimeSpent` | Busyness per day and hour (0-100), live busyness and "People typically spend ..." when Google has them |
| `description` | Owner or Google description |
| `about` | "About" attributes by group: service options, accessibility, offerings, payments, amenities, crowd, parking, pets ... |
| `menuUrl`, `reservationLinks`, `orderLinks` | Menu link, booking partners (OpenTable, Resy, Toast ...), order online / reserve buttons |
| `photosCount`, `imageUrls` | Number of photos and up to 10 image URLs |
| `ownerName`, `reviewTopics`, `secondaryHours`, `peopleAlsoSearch` | Owner profile name, review topics with mention counts, happy hours / delivery hours, related places |

Every review (`"type": "review"`): `reviewId`, `rating`, `text`, `originalText` + `textTranslated`, `language`,
`publishedAt` (exact UTC time), `publishedAtText` ("3 days ago"), `updatedAt`, `reviewerName`, `reviewerId`,
`reviewerProfileUrl`, `reviewerPhotoUrl`, `reviewerIsLocalGuide`, `reviewerReviewCount`, `reviewerPhotoCount`,
`photos`, `likesCount`, `reviewContext` (dining answers such as "Price per person", Food/Service/Atmosphere
scores), `ownerResponse`, `ownerResponseDate`, `reviewUrl`, `reviewsSort`, and the place (`placeId`, `placeName`,
`placeAddress`, `cid`, `googleMapsUrl`).

### How to scrape Google Maps places

1. Enter one or more **search terms** ("coffee shop", "dentist", "roofing contractor").
2. Enter a **location**: a city, address, ZIP code or region ("Austin, TX", "10001", "Berlin, Germany"). Or give
   **coordinates** (`30.2672,-97.7431`) and a **zoom level** to search around a point.
3. Set **Max places per search** and **Max places in total**, then run. Each term is searched separately; places
   that appear in several searches are saved once.

You can also paste Google Maps **URLs** (place pages, search pages with `@lat,lng,zoom`, `?cid=` links, short
links) or **place IDs / CIDs** to scrape specific places. Set **language** (`hl`, e.g. `de`) and **country**
(`gl`, e.g. `gb`) to get names, categories and hours the way local users see them.

### How many places does one search return?

Google ends one Maps result list after roughly 160-260 places (tested: "bakery" around downtown Chicago 159 unique
places from 11 pages, "restaurants in Chicago, IL" 259 from 14 pages). The actor pages until Google's list ends or
your limit is reached. **Deep search** splits the location into a grid of smaller map views (16 by default) and
searches each one, keeping only places inside your area and removing duplicates. It helps when an area holds more
matches than one list shows: "restaurants in Chicago, IL" with 16 cells reached our 1,000-place test limit after
79 pages (vs 259 from one list). For small areas a single list usually already has them all (bakeries in a
15 km area: 164 with 4 cells vs 159 from one list). Deep search costs one request per page per cell, so keep
**Max places per search** at what you need.

### How to scrape Google Maps reviews

Set **Reviews per place** (e.g. 5) and a **sort** (newest, most relevant, highest or lowest rating). Without a
signed-in browser, Google serves the **first 5-8 reviews of each sort order** with the place: Google's full review
list needs a browser security token that plain HTTP clients do not get. If you ask for more than one sort
returns, the actor adds the other sort orders, so you get up to about 20-30 distinct reviews per place. For a
complete review history of a place (thousands of reviews), use a dedicated browser-based reviews scraper.

**Only reviews newer than** (`reviewsNewerThan`: `2026-10-01`, `7 days`, `24 hours`) keeps only recent reviews and
forces the newest-first order. Scheduled daily, it catches every new review of places that get fewer than about
5-8 reviews a day.

### Monitor changes on a schedule

Turn on **Only new or changed items** and save the input as a task with a Schedule (e.g. daily). Each run then
outputs only places that are new to this search or whose rating, review count or closed status changed, and only
reviews it has not output before; unchanged places are skipped before any detail request and are not charged. New
items carry `changeType: "new"`, changed places `changeType: "changed"` and `previousValues` with the old values
(`{"reviewCount": 1408}`). Google re-ranks a list a little on every request, so a watch over only the top N places
sees some re-ranked places as new (top 40 dentists in Austin: 15 of 40 on the second run); set **Max places per
search** to 200 to watch the whole list. Give each watch its own **Monitoring key** if several schedules search
the same thing.

### Input example

```json
{
  "searchTerms": ["coffee shop", "bakery"],
  "location": "Austin, TX",
  "maxPlacesPerSearch": 100,
  "maxItems": 200,
  "includeDetails": true,
  "maxReviewsPerPlace": 5,
  "reviewsSort": "newest",
  "language": "en",
  "country": "us"
}
```

### Output example

A place with details (shortened):

```json
{
  "type": "place",
  "placeId": "ChIJ-UsJsFTTD4gR9wNj_lK4NEQ",
  "cid": "4914755759962784759",
  "featureId": "0x880fd354b0094bf9:0x4434b852fe6303f7",
  "name": "Gino's East",
  "category": "Pizza restaurant",
  "categories": ["Pizza restaurant", "Bar", "Caterer", "Italian restaurant"],
  "address": "162 E Superior St, Chicago, IL 60611",
  "street": "162 E Superior St",
  "neighborhood": "Streeterville",
  "city": "Chicago",
  "postalCode": "60611",
  "state": "Illinois",
  "countryCode": "US",
  "latitude": 41.8958861,
  "longitude": -87.6230528,
  "plusCode": "V9WG+9Q Chicago, Illinois",
  "phone": "(312) 266-3337",
  "phoneInternational": "+13122663337",
  "website": "http://www.ginoseast.com/",
  "rating": 4.7,
  "reviewCount": 21103,
  "priceLevel": "$20–30",
  "openingHours": [{"day": "Monday", "hours": "11 AM–9 PM"}, {"day": "Tuesday", "hours": "11 AM–9 PM"}],
  "openStatus": "Open · Closes 9 PM",
  "permanentlyClosed": false,
  "temporarilyClosed": false,
  "summary": "Deep-dish pizzas, salads & sandwiches",
  "isClaimed": true,
  "googleMapsUrl": "https://www.google.com/maps/place/?q=place_id:ChIJ-UsJsFTTD4gR9wNj_lK4NEQ",
  "searchTerm": "pizza",
  "searchLocation": "Chicago, IL",
  "rank": 4,
  "ratingHistogram": {"1": 468, "2": 339, "3": 779, "4": 2419, "5": 17098},
  "popularTimes": {"Friday": [{"hour": 17, "occupancyPercent": 76}, {"hour": 18, "occupancyPercent": 83}]},
  "popularTimesLiveText": "Not busy",
  "typicalTimeSpent": "People typically spend 45 min to 2 hr here",
  "about": {"Service options": ["Outdoor seating", "Delivery", "Takeout", "Dine-in"],
            "Highlights": ["Great beer selection", "Rooftop seating"]},
  "menuUrl": "https://www.toasttab.com/bravo-restaurants-group-ginos-east-superior-street/v3/",
  "reservationLinks": [{"url": "https://tables.toasttab.com/restaurants/bedbc320-282f-4ef5-9ae8-b8ed91a0aa6e/findTime",
                        "domain": "toasttab.com"}],
  "photosCount": 11696,
  "reviewTopics": [{"topic": "deep dish pizza", "mentions": 3306}],
  "detailsScraped": true
}
```

A review:

```json
{
  "type": "review",
  "reviewId": "Ci9DQUlRQUNvZENodHljRjlvT21relZHNWxkRkpXYlU5Rk1USndNazlaVkRWQ2MwRRAB",
  "rating": 1,
  "text": "was my birthday, & I came from out of town to Chicago really excited to try this place ...",
  "language": "en",
  "publishedAt": "2026-08-12T02:56:37Z",
  "publishedAtText": "2 months ago",
  "reviewerName": "Georgiana Rocha",
  "reviewerProfileUrl": "https://www.google.com/maps/contrib/101011296062674741768?hl=en-US",
  "reviewerIsLocalGuide": true,
  "reviewerReviewCount": 14,
  "photos": ["https://lh3.googleusercontent.com/grass-cs/AABkmLcog0hX...=k-no"],
  "likesCount": 0,
  "reviewContext": {"Service": 1},
  "ownerResponse": "We're truly sorry to hear about your experience, Georgiana. Celebrating a birthday should be special ...",
  "ownerResponseDate": "2026-09-01T14:54:24Z",
  "placeId": "ChIJh3i0BqYsDogR6yp0y6TFlOQ",
  "placeName": "Giordano's",
  "reviewsSort": "lowestRanking"
}
```

### How much does it cost to scrape Google Maps?

| Event | Price | When |
|---|---|---|
| `place` | $0.0025 | Each place saved ($2.50 per 1,000) |
| `place-details` | $0.0015 | Extra per place with **Include place details** on |
| `review` | $0.0005 | Each review saved |

Platform usage is billed to your account at Apify's rates and is small: the default datacenter proxy is enough,
a search page brings 20 places in one request, and measured runs used about $0.01 per 1,000 places (list only) and
about $0.05 per 1,000 places with details. Example: 1,000 places with details and 5 reviews each ≈ $2.50 + $1.50 +
$2.50 + ~$0.05 usage.

### Does it need a Google API key, a login or a special proxy?

No. It reads the same public data a logged-out visitor sees on google.com/maps, with Apify's ordinary datacenter
proxy (US exits by default). No Google Places API key, no Google account, no residential or SERP proxy. Google
sometimes answers a session with a reduced record (no price level or popular times); the actor detects that and
asks again through another session.

### Limitations

- Reviews: the first 5-8 per sort order (about 20-30 per place across all sorts), see above.
- One result list ends after about 160-260 places; deep search goes further in big areas.
- Price level, popular times, menu and reservation links exist only for places where Google shows them.
- Closure flags and the status line are read from Google's text in the requested language.
- Emails and social profiles from business websites are not collected (this actor does not visit websites).

### Is it legal to scrape Google Maps?

The actor collects publicly visible business listings (names, addresses, phones, hours, ratings) without logging in,
at a modest request rate. Reviews include the public display names and profile links that Google shows next to
them. Check the terms and data-protection rules that apply to your use of the data, especially for republishing or
contacting people.

# Actor input Schema

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

What to search for on Google Maps, one term per line, e.g. 'coffee shop', 'dentist', 'plumber'. Each term is searched in the location below.

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

City, address, ZIP code or region to search in, e.g. 'Austin, TX', '10001' or 'Berlin, Germany'. Leave empty when you use coordinates or the location is part of the search term.

## `coordinates` (type: `string`):

Search around a point instead of a named location: 'latitude,longitude', e.g. '30.2672,-97.7431'. Used together with the zoom level.

## `zoom` (type: `integer`):

Map zoom for coordinates: 12 = whole city, 14 = district (default), 16 = a few streets.

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

Stop each search term after this many places. One Google Maps list ends after about 160-260 places; turn on deep search to go further in big areas.

## `deepSearch` (type: `boolean`):

Split the location into a grid of smaller map views and search each one, to find more places than one Google list shows (a single list stops at about 160-260). Costs more requests; places are deduplicated.

## `maxGridCells` (type: `integer`):

Number of map views the area is split into for deep search (4, 9, 16, 25, ...). More cells find more places in dense cities.

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

Google Maps place URLs (google.com/maps/place/...), search URLs (google.com/maps/search/...), '?cid=' links or maps.app.goo.gl short links.

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

Google place IDs (ChIJ...), CIDs (the number in '?cid=') or feature ids (0x...:0x...). Each one is scraped as a single place.

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

Stop after this many places across all searches and inputs (reviews are limited separately below). Keeps cost predictable.

## `includeDetails` (type: `boolean`):

Open each place's own record for the extra fields: rating histogram, popular times, description, 'About' attributes (amenities, accessibility, payments...), menu/reservation/order links, photo count and image URLs, owner name, review topics. Charged as an extra 'place-details' event per place.

## `maxReviewsPerPlace` (type: `integer`):

Output up to this many reviews per place (0 = none). Without a signed-in browser Google serves the first 5-8 reviews of each sort order, so up to about 8 reviews per sort; above that the actor adds the other sort orders (about 20-30 distinct reviews at most).

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

Which reviews Google returns first.

## `reviewsNewerThan` (type: `string`):

Only reviews newer than: a date (YYYY-MM-DD) or a period such as '7 days' or '24 hours'. Forces the newest-first order.

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

Language of names, categories, opening hours and review translations (Google 'hl' code, e.g. en, de, fr, es).

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

Two-letter country code Google localises results for (Google 'gl', e.g. us, gb, de).

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

Remember what earlier runs of this search returned and output only new places or places whose rating, review count or closed status changed (and only new reviews). Use with a Schedule; unchanged items are not charged.

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

Name for this watch, so several schedules on the same search stay separate; defaults to a key derived from the search input.

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

Apify datacenter proxy (default, US exits) works for Google Maps; no SERP or residential proxy needed.

## Actor input object example

```json
{
  "searchTerms": [
    "coffee shop"
  ],
  "location": "Austin, TX",
  "maxPlacesPerSearch": 120,
  "deepSearch": false,
  "maxGridCells": 16,
  "maxItems": 100,
  "includeDetails": false,
  "maxReviewsPerPlace": 0,
  "reviewsSort": "newest",
  "language": "en",
  "country": "us",
  "onlyNewOrChanged": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

No description

# 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": "Austin, TX",
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("tortuga/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": "Austin, TX",
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("tortuga/google-maps-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": "Austin, TX",
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call tortuga/google-maps-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,tortuga/google-maps-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/LhosaLu5OfJKXWvz1/builds/jSCxDA64mXO5Yge5N/openapi.json
