# Google Maps Scraper - Business Data, Reviews & Leads (`buff_pineapple/google-maps-scraper`) Actor

Scrape Google Maps business listings without an API key — name, address, phone, website, rating, reviews, photos, hours, amenities & GPS. Precision targeting (batch, map URLs, custom area) + optional add-ons: emails, socials, contacts & ad intelligence. Export to CSV, Excel & JSON.

- **URL**: https://apify.com/buff_pineapple/google-maps-scraper.md
- **Developed by:** [yossef Nagy](https://apify.com/buff_pineapple) (community)
- **Categories:** Lead generation, Business, Developer tools
- **Features:** Limited permissions
- **Stats:** 439 total users, 29 monthly users, 95.2% runs succeeded, 4 bookmarks
- **User rating**: No ratings yet

## Pricing

from $4.00 / 1,000 place results

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

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

## What's an Apify Actor?

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

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

## How to integrate an Actor?

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

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

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

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

# README

## Google Maps Scraper

Extract business data from Google Maps at scale — **names, addresses, phone numbers, websites, ratings, reviews, photos, opening hours, amenities, coordinates and more** — for any type of business in any location. Extraction uses Google Maps' and Google Search's internal HTTP endpoints; no browser is launched.

It also ships powerful, optional layers: **precise targeting** (batch queries, map URLs, or a drawn area), **deep reviews**, **complete place data**, plus experimental **lead enrichment** (emails, socials, people/contacts) and **ad intelligence** (is the business advertising on Meta / Google right now?).

### What it costs

This Actor uses pay per event pricing. The same flat prices apply to every customer; Apify Console shows the effective prices and your estimated run cost before you start. The charge units are:

| Event | Price | Charged unit |
| --- | ---: | --- |
| Result | $0.004 | Each saved business row in the default dataset. |
| Actor start | $0.00005 | Each GB of run memory, rounded up (minimum one). |
| Reviews | $0.0005 | Each real delivered review (author, 1-5 rating, and text or date), up to `reviewsLimit` per business. Highlights without an author are never billed. |
| Emails | $0.005 | Each distinct delivered email address on the business's own website domain or on a free email provider (such as Gmail, Outlook or Yahoo), only when you turn on `includeEmails` or `emailOnly`. Other addresses found on its pages (an agency's, a vendor's, a booking platform's or another location's mailbox) are returned but not billed. A business whose Maps website is a social profile, directory, site-builder, booking, listing or ordering page, or its store page on a shopping centre's, landlord's, food hall's or shared directory's site (for example `highstreetplace.com/merchant/gracenote-coffee/`), has no own domain, so only its free-mail addresses are billed; the landlord's or platform's mailboxes found there are returned free. |
| Social profiles | $0.006 | Each distinct delivered social profile URL, only when you turn on `includeSocials` or `socialOnly`. |
| People / contacts | $0.008 | Each saved business with at least one proven person (`includePersonnel` / `onlyWithPersonnel`): someone with their own email, LinkedIn profile or direct phone, or someone with a role title (or a professional prefix such as Dr.) in a real staff list on the site, such as a team page or a staff grid. A name taken from a page heading, a paragraph, a footer, a menu, a client testimonial or a button does not count, and neither does the business's own structured-data record; see People / contacts below. The business's own phone, email and social profiles stay in `business_lead` and are never shown as a person's. Emails and socials it returns are not billed unless you enabled them. |
| Meta ads | $0.004 | Each saved business whose Meta Ad Library check identified the business's own advertiser, including one with no currently active ads. A search that finds nothing, or only ads that cannot be attributed to the business, is returned as null and is not billed. |
| Google ads | $0.0008 | Each saved business with a website whose Google Ads Transparency lookup completed and could be attributed to it, including one where no ads were found. The advertiser is used when its name points at your business, or when it is the only advertiser on your website domain and is not an agency, retail-media brand or ordering/booking platform (an agency, retail-media brand or ordering platform that links to your website is never used). A business without its own website domain, an unreachable lookup, or ads that cannot be attributed to the business is returned as null and is not billed. |

Place details, photos, amenities, place extras and popular times have no separate fee; they are included in the Result price. Add-ons are charged per delivered unit, after the saved business has been written to the dataset. They are never charged once per run, and an attempted but inconclusive check is not billed. Rows removed by a filter and skipped rows are not saved and not charged. When your spending limit is reached, the last business is saved with all its data and only the add-on units that fit are charged (see below). Start with `maxResults: 10`, review the resulting charge breakdown, then scale the same input. For example, 10 saved businesses with five real reviews, two email addresses and one completed Meta check cost $0.0566 in event charges at 2 GB memory ($0.04 + $0.0001 + $0.0025 + $0.01 + $0.004), before any other enabled add-ons.

#### Which options switch on a paid add-on

Some filters imply an add-on check. The table shows exactly what each option runs and whether it is billed.

| You turn on | What also runs | Billed? |
| --- | --- | --- |
| `includeEmails` | Website crawl for email addresses | Yes, per distinct delivered email on the business's own website domain or a free email provider |
| `emailOnly` | Same as `includeEmails`, plus the filter | Yes, per distinct delivered email on the business's own website domain or a free email provider (a business whose only email is on another domain is saved and billed the Result price only) |
| `includeSocials` | Website crawl for social profiles | Yes, per distinct delivered social profile URL |
| `socialOnly` | Same as `includeSocials`, plus the filter | Yes, per distinct delivered social profile URL (a profile that is itself the business's Google Maps website is free) |
| `includePersonnel` | People search on the business website; it also finds emails and social profiles | People/contacts only. The emails and socials it returns are included in the row but are not billed unless you also enabled them above |
| `onlyWithPersonnel` | Same as `includePersonnel`, plus the filter | People/contacts only, as above |
| `includeMetaAds` / `includeGoogleAds` | The matching ad check | Yes, per completed check as described in the table above |
| `onlyRunningAds` on its own | Both the Meta and the Google ad check | **Yes.** Every saved business is, by definition, running ads on at least one source, and each completed Meta or Google check on a saved business is billed at the ad prices above, even though you did not tick the ad options |
| `onlyRunningAds` with `includeMetaAds` or `includeGoogleAds` | Only the source(s) you selected | Yes, for the selected source(s) only |
| `onlyWithWebsite`, `onlyWithoutWebsite` | Nothing extra | No |

Filters never charge for businesses they remove. With a filter on, the Actor looks at a bounded amount of extra places (roughly up to three times `maxResults`) to fill the shortfall, and you pay only for the saved rows.

What is never billed as an add-on unit: reviews that are only a photo or Street View link, a Google interface label or a continuation token; review highlight snippets without an author; email addresses that are neither on the business's own website domain nor on a free email provider (they are returned free); email addresses from placeholder or template domains, and malformed addresses glued to a real one in a page's visible text (a label or phone number stuck in front of a mailbox, a city or state name stuck in front of another mailbox of the same business such as `austinhello@` next to `hello@`, or a plain word stuck in front of a bare label mailbox; an address the site itself links or declares, such as `skininfo@` in its structured contact data next to `info@`, is a real mailbox and is kept; two genuinely different mailboxes such as `events@` and `privateevents@` are both kept); official accounts of site-builder and hosting platforms found in templates and footers (Wix, Squarespace, Shopify, GoDaddy, Weebly, Webflow, Duda, Luxury Presence and similar, unless the business is itself that platform); official accounts of booking, listing and ordering platforms (Booksy, Vagaro, Zocdoc, Calendly, OpenTable and similar) found on its pages; truncated or malformed social links; a social profile that is the business's own Maps website; contact names that are page headings, platform names or legal-page words, that are taken from a page footer, a legal line, a menu, a list of links, a client testimonial or a one-off line of text, or that are not shaped like a person's name (an email or web address, a widget or form label, button text, a service or class heading, a lone first name with no role, email, phone or LinkedIn); a person who is not proven (no email, LinkedIn or direct phone of their own and no role title inside a real staff list, see People / contacts below); a name that is really a heading or a business (an institution or education line such as "Baylor University", a practice-area or service heading such as "Atlanta Teeth" / "Whitening Specialist" or "Dayton Family Dentist", the business's own name or structured-data record, a blog or page author named only in the page's metadata); a WhatsApp share button (`api.whatsapp.com/send?text=...`, or a `send` link that names no phone number); and a People unit for a name that was only guessed from its position next to an email or phone number.

If you set a maximum total charge for the run (`maxTotalChargeUsd`), it is never exceeded, and every business the run saves is saved with all its data. When a business's add-on units no longer all fit the remaining limit, that business is still saved complete and is the last one: only the add-on units that still fit are charged, in this order: reviews, emails, social profiles, people, Meta ad check, Google ad check; the rest are included free. The run then stops and its status message says "Saved N result(s); the run's charge limit (maxTotalChargeUsd) was reached, so the run stopped there. The last result was saved with all its data; M of its add-on unit(s) did not fit the limit and were included free of charge." When not even the next business's Result price fits, the run stops before it and says so. If the run timeout is also reached in the same run, the message keeps that text and adds how the timeout affected what was saved (for example that 3 of the 12 saved results were saved before their enrichment could finish, so their add-on fields may be empty); raise the run timeout as well as the limit for complete results. On a tight limit the Actor enriches only about as many businesses as the limit can pay for.

### What's new

- **Add-ons are charged per delivered unit** (reviews, emails, social profiles, people, ad checks) instead of a flat fee per run. The four small per-run fees for place details, photos, place extras and popular times are gone.
- **`google_maps_url` now carries the business name and coordinates**, so a saved result can be pasted back into `startUrls` and resolves to the same place.
- **`found_via` is always filled**, including for a plain `query` run (the dataset schema description still says it is absent there; that text is out of date).
- **Businesses with accented or non-Latin names** (for example Café or 渋谷カフェ) now get their full place details.
- **Breaking change: `hours` keys are always the English weekday names** (`monday` ... `sunday`). The previous version used the result language for the keys (`lundi` ... `dimanche`, `montag` ... `sonntag`); update any code that reads localized keys. The opening-hours text itself stays in your language.
- **Ad intelligence is stricter about whose ads it reports.** A Google advertiser must resemble your business or its website domain, or be the only advertiser on that domain and not a known agency, retail-media brand or platform, and a Meta advertiser must be tied to the business (its Facebook page, or an exact page name backed up by its website); otherwise the ad fields are `null` and not billed. `google_ads_active_count` counts only ads shown in the past 30 days when Google returns a single page (see Ad intelligence below).
- **Reviews work again**: full review lists up to `reviewsLimit` from Google's own review list, a real newest-first sort, and rating / keyword filters that count toward the limit (see Reviews). No browser is launched and nothing is stored for reviews.
- **Place links are fetched directly and completely.** Every supported form returns the place it points to with its full details: full URLs with or without `@lat,lng` (also when a spreadsheet export or a redirect link percent-encoded the colons and slashes in them), links with Google's short `/g/` place id (including ids that contain an underscore, such as `/g/1v76_180`), `?cid=`, `?ftid=`, `?q=place_id:` and `query_place_id` links, and maps.app.goo.gl share links. The run's status message names any requested place that could not be fetched or came back incomplete (when amenities are requested, also one whose amenities are still only accessibility items or empty after three lookups); a link that only marks a map position, with no place name or id, cannot be looked up and is named there too. A link that the run timeout stopped before it could be looked up is not named as unfetchable: the status message reports the run timeout instead. A link that really could not be fetched is still named in a run the timeout cut short: when such a run saved no row, it ends as failed with the run-timeout message, and the same message names the lost link.
- **Impossible filter combinations stop immediately** with a clear message instead of running and returning nothing.
- **Spending limits keep every saved business complete.** When the limit is reached, the last business is saved with all its data (its add-on units that do not fit are free), the run stops cleanly and the status message says so (and names the run timeout too when it was also reached in the same run).
- **Emails are billed only on the business's own website domain or a free email provider**; agency, vendor, booking-platform and other-location mailboxes are returned free, and so are a landlord's mailboxes when the Maps website is the business's store page on a shopping centre's or landlord's site. An address the site declares itself (an email link, its structured contact data or a protected email address) is always returned.
- **People / contacts are billed only for proven people.** A person counts when they have their own email, LinkedIn profile or a direct phone printed in their own staff card, or a role title (or a professional prefix such as Dr.) inside a real staff list: a team or staff grid, a staff page, their own profile page in a staff directory, or the site's structured-data record for that person. Names are not taken from page footers, menus, lists of links, buttons, client testimonials, one-off text or headings: a page's title or main heading such as "Dayton Family Dentist" is never a person, also when it starts with a first name. An institution or education line ("Baylor University"), a practice-area or service heading ("Atlanta Teeth" / "Whitening Specialist"), the business's own structured-data record (also when the business is named after a person, the record is typed both Person and Organization, or the Maps name adds a city) and a blog author named only in a page's metadata are not staff either, and the business's own phone, email and social profiles are never put on a person. Whether a first name is common plays no part: "Chloe Martin" in a team grid is billed like anyone else. Cards that only look like a grid of people are not staff: products, classes, colours, software, awards, brands and packages ("Agreeable Gray" / "Designer Favorite"), office and "Areas we serve" cards ("Morgan Hill" / "Family Dentist near Morgan Hill") and department cards ("Guest Experience", "Patient Care"). Real staff on a page without a proper staff list can be returned but not billed (see People / contacts). More team-page layouts are read (Squarespace and WordPress team pages, staff directories that put a name heading above its role line).
- **Social profiles are attributed more carefully.** A Google Maps website that is a link inside a social account (`instagram.com/handle/tagged`, `facebook.com/page/posts`) is read as that account's profile, so the place passes `socialOnly` and the profile is free; a booking, listing or ordering platform's own accounts (`facebook.com/booksypolska`, `instagram.com/booksybiz`) are no longer returned or billed as the business's.
- **A profile that a site links through one of its tabs is returned as the profile itself.** `youtube.com/channel/<id>/videos`, `youtube.com/@handle/featured`, `instagram.com/<handle>/reels`, `x.com/<handle>/media`, `facebook.com/<page>/about` and `linkedin.com/company/<name>/about` give `youtube.com/channel/<id>`, `youtube.com/@handle`, `instagram.com/<handle>` and so on, billed once with the profile; 1.11.31 returned the tab address as written. A link to a single Instagram post or reel, X post, TikTok video or YouTube video is not a profile. A Facebook link to a post, photo, video or event of a page, Facebook's old `/pg/<page>/...` form and a page tab such as `menu`, `shop` or `jobs` give the page's profile (`facebook.com/<page>`), billed once with it, and a Facebook link that names no page (`facebook.com/events/<id>`, `photo/?fbid=`) is not returned. A tab on a YouTube vanity address (`youtube.com/<name>/videos`) gives `youtube.com/<name>`.
- **A WhatsApp share button is not the business's WhatsApp.** `api.whatsapp.com/send?text=...` (a "share this page" widget) and any `send` link that names no phone number are no longer returned or billed; 1.11.31 returned them as the business's WhatsApp. A business number (`wa.me/<digits>`, `send?phone=<digits>`) or short link (`api.whatsapp.com/message/<code>`) is kept and billed as before.
- **Opening hours of businesses that hide their address** (plumbers, locksmiths and other service-area businesses) are returned, and so is their phone when the place details are its only source. Before, `hours` was empty for them (also in 1.11.31) although Google shows it; their `address` stays empty because Google does not publish one.
- **Rows saved with basic data only are named in the status message**, with their number, instead of ending without a message.
- **Meta sample titles** that were raw catalog templates such as `{{product.name}}` now read "No title available for this ad".
- **A custom `proxyUrl`** is tested with up to 3 attempts before the run starts, so one dropped connection no longer fails the run.
- **Run logs no longer contain proxy passwords or API tokens.**

***

### What does it do?

1. **Searches Google Maps** for your query and covers the whole area with a **grid search**, finding businesses that a single search page misses.
2. **Extracts a clean record** for every business — the full place profile.
3. **Optionally enriches** each business with deeper place data, full reviews, website contact data, the people who work there, and live ad-spend signals.

***

### Features

- 🗺️ **Comprehensive place data** — name, address, phone, website, rating, review count, category list, coordinates, opening hours, price level, **photos, amenities, Plus Code, permanently-closed status**.
- 🎯 **Precise targeting** — search by a plain query, a **batch of queries**, **Google Maps URLs**, or a **custom area** (lat/lng + radius, or a GeoJSON polygon).
- ⭐ **Reviews** — up to `reviewsLimit` reviews per place from Google's own review list, newest first or in Google's relevant order: text, rating, date and author plus owner responses, reviewer stats, language and review photos, with **rating / keyword filters**.
- 📩 **Lead enrichment (experimental)** — emails (MX-validated) and social profiles harvested from each business's own website.
- 👤 **People / contacts (experimental)** — the actual people at a business with their own name, title, email, phone and LinkedIn.
- 📣 **Ad intelligence (experimental)** — detect which businesses are **actively running ads** on Meta (Facebook/Instagram) and Google (Search/YouTube/Display), with sample creatives.
- ⚡ **HTTP-only** — no browser is launched, reviews included. Residential proxy remains the reliable default for Maps traffic.

***

### Quick start

Just enter what you'd type in the Google Maps search box — **include the location**:

```
restaurants in New York
```

```
dentists in Miami, FL
```

```
coffee shops 90210
```

That's it. Everything else has a sensible default. Turn on the optional layers below only when you need them.

***

### Targeting (how to choose what to scrape)

You can target in four ways — use whichever fits:

| Input | Use it for |
|-------|-----------|
| `query` | A single search, e.g. `plumbers in Chicago`. The location is auto-detected. |
| `searchQueries` | A **batch** of searches in one run, e.g. `["dentists in Miami", "dentists in Orlando"]`. Results are de-duplicated across queries. |
| `startUrls` | Direct **Google Maps URLs** (place or search URLs). |
| `customGeolocation` | A **custom area** — either a point + radius `{ "lat": 25.77, "lng": -80.19, "radiusMeters": 1500 }` or a GeoJSON `Polygon` / `MultiPolygon` (coordinates in `[lng, lat]` order). |

If you provide `searchQueries` or `startUrls` you can leave `query` empty. A `customGeolocation` only says *where* to search, so it needs `query` as well (the category to look for): when the area is the only target and `query` is empty the run fails at once with a message, and when `searchQueries` or `startUrls` are given the area search is skipped with a warning and those run as usual. A Google Maps place link can be pasted in any common form: the full URL from your browser's address bar (with or without its `@<lat>,<lng>` part, and also when a spreadsheet export or a redirect link percent-encoded its colons and slashes), a maps.app.goo.gl share link, a `?cid=` or `?ftid=` link, a `?q=place_id:` or `query_place_id` link, or the `google_maps_url` from an earlier result; a link that holds only a `/g/` id needs its name and coordinates (the full URL) to be found. A requested place that could not be fetched, or that came back with incomplete details, is named in the run's status message, also when a spending-limit or timeout status is shown (a link that the run timeout stopped before it could be looked up is not named: the status message reports the timeout). A link that only marks a map position (for example `.../maps/place/40.6892494,-74.0445004`, no place name or id) cannot be looked up and is named there as well.

***

### Output

Each business is one dataset item. Base fields (always present when `Include Place Details` is on):

```json
{
  "name": "Joe's Pizza",
  "address": "7 Carmine St, New York, NY 10014",
  "phone": "+1 212-366-1182",
  "website": "https://www.joespizzanyc.com",
  "rating": 4.5,
  "reviews_count": 12847,
  "category": "Pizza restaurant",
  "categories": ["Pizza restaurant", "Italian restaurant"],
  "latitude": 40.7304,
  "longitude": -74.0022,
  "place_id": "ChIJr3k0v6VZwokRPCxBJnIcdTA",
  "google_maps_url": "https://www.google.com/maps/place/Joe%27s%20Pizza/@40.7304000,-74.0022000,15z/?q=place_id:ChIJr3k0v6VZwokRPCxBJnIcdTA",
  "hours": { "monday": "10 AM-2 AM", "tuesday": "10 AM-2 AM" },
  "price_level": "$10-20",
  "photos": ["https://lh3.googleusercontent.com/..."],
  "amenities": ["Outdoor seating", "Takeout", "Wheelchair accessible entrance"],
  "plus_code": "76QXQR66+RC"
}
```

`google_maps_url` includes the business name and its coordinates, so you can paste it back into `startUrls`; it falls back to the plain `?q=place_id:` form when a result has no usable coordinates. `hours` keys are always the English weekday names (`monday` … `sunday`) whatever the result `language`; the hour text itself stays in the result language. This is a breaking change for non-English integrations: the previous version keyed `hours` by the local weekday name (`lundi` … `dimanche` in French, `montag` … `sonntag` in German). Only when Google sends no usable day number for a day does the lower-cased local day name remain the key. `found_via` says which target produced the row (`query` for a single plain query, `query: <text>` for each `searchQueries` entry, `startUrl place`, `startUrl search` or `customGeolocation`). It is filled on every row, although the field's description in the dataset schema still says it is absent for a plain single-query run.

Additional fields appear when the matching option is enabled: `reviews`, `permanently_closed`, `popular_times`, the lead-enrichment columns (`email`, `emails`, `facebook` … `whatsapp`, `website_reachable`), the people columns (`business_lead`, `contacts`), and the ad-intelligence columns (`meta_ads_*`, `google_ads_*`). Anything not found is `null` — the base scrape is never affected.

***

### Input reference

#### Results & detail

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `maxResults` | integer | 100 | Max businesses to extract (0 = unlimited). |
| `language` | string | `en` | Two-letter result language (Google `hl`). |
| `zoom` | integer | 13 | Search granularity (1–21). Values 1–10 share the widest, coarsest bounded grid; 11–15 progressively refine it; 16–21 share the finest bounded grid. |
| `includeDetails` | boolean | true | Fetch full place details (hours, phone, website, price level, photos…). |

#### Complete place data

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `includePhotos` | boolean | true | Include up to 10 photo URLs per place. |
| `includePlaceExtras` | boolean | true | Include amenities, Plus Code and permanently-closed status. |
| `includePopularTimes` | boolean | false | Experimental: Google's place data almost never includes the popular-times histogram, so `popular_times` is `null` for virtually all places. No separate fee. |

#### Reviews

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `includeReviews` | boolean | false | Extract reviews for each business from Google's review list. Billed per delivered review that has an author. |
| `reviewsLimit` | integer | 5 | Max reviews per business (up to 1,000). Counts reviews that pass the rating and keyword filters. |
| `minReviewRating` | integer | 0 | Keep only reviews rated ≥ this (1–5; 0 = off). Applied while paging; with a filter on, at most the first 2,000 reviews of a place are scanned (if that limit is reached before `reviewsLimit` matching reviews are found, the status message says so). |
| `reviewKeyword` | string | — | Keep only reviews whose text contains this keyword. Applied while paging; with a filter on, at most the first 2,000 reviews of a place are scanned (if that limit is reached before `reviewsLimit` matching reviews are found, the status message says so). |
| `reviewsSort` | select | newest | `newest` (most recent first) or `relevant` (Google's default order). |

Each review includes `review_id`, `author`, `author_photo`, `rating`, `date`, `text`, and (when present) `owner_response_text`, `owner_response_date`, `reviewer_review_count`, `reviewer_is_local_guide`, `review_language` and `review_images`. `review_translated` is always null. A review is billed only when it has an author, a 1–5 rating and text or a date. If a place's review list cannot be fetched, stops part-way with nothing usable, or the run timeout comes first, the few reviews embedded in the place data are returned instead when they are available (filtered and capped at `reviewsLimit`; highlight snippets without an author are returned but not billed) and the status message says so. A place that Google itself shows without any review (no rating and no review count) is not asked for its review list, so it costs no review request; billing is unchanged, only delivered reviews are billed.

#### Lead enrichment (experimental add-on)

Visits each business's **own website** (plus contact/about/imprint pages) to extract contact data. Off by default; never changes the base scrape.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `includeEmails` | boolean | false | Find email addresses (deduplicated and MX-validated). Billed per delivered address on the business's own website domain or a free email provider; other addresses are returned free. A business whose Maps website is a social profile, directory, site-builder, booking, listing or ordering page, or a store page on a shopping centre's or landlord's site, has no own domain, so only its free-mail addresses are billed. |
| `includeSocials` | boolean | false | Find social profiles (Facebook, Instagram, LinkedIn, X/Twitter, YouTube, TikTok, WhatsApp). The official accounts of site builders and of booking, listing and ordering platforms are not returned as the business's. A link to one of a profile's tabs (for example `youtube.com/channel/<id>/videos`) is returned as the profile itself, and so is a Facebook link to a post, photo, video or event of a page (its page profile); a link to a single Instagram post or reel, X post, TikTok or YouTube video is not a profile, a Facebook link that names no page (`facebook.com/events/<id>`, `photo/?fbid=`) is not returned, and neither is a WhatsApp share button (`api.whatsapp.com/send?text=...`). |
| `emailOnly` | boolean | false | Keep only businesses with an email (implies `includeEmails`, so emails on the business's own website domain or a free email provider are billed per delivered address; other addresses are returned free). |
| `socialOnly` | boolean | false | Keep only businesses with a social profile (implies `includeSocials`, so social profiles are billed per delivered URL). Can be combined with `onlyWithoutWebsite` to find businesses whose only listed website is a social profile or a link inside one (such as `instagram.com/handle/tagged`, read as that account's profile). |
| `onlyWithWebsite` | boolean | false | Keep only businesses that have a real website (including a page on a site builder such as wixsite.com or square.site, but not a social profile or directory listing). |
| `onlyWithoutWebsite` | boolean | false | Keep only businesses **without** a website (prospects for web/design agencies). A page on a site builder (for example wixsite.com, square.site, weebly.com, business.site) counts as a website; a social profile or directory listing does not. It cannot be combined with `onlyWithWebsite`, `emailOnly` or `onlyWithPersonnel`; those inputs stop the run immediately with a message. |
| `maxPagesPerSite` | integer | 4 | Advanced: max pages crawled per website (1–10). |

#### People / contacts (experimental add-on)

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `includePersonnel` | boolean | false | Extract the people at each business (name, title, their own email/phone/LinkedIn). The business's own phone, email and social profiles stay in `business_lead`. |
| `maxContactsPerBusiness` | integer | 10 | Max contacts per business (highest-confidence first). Within a confidence tier, a contact that earns the People charge comes first, then a contact with personal-name evidence of its own (Dr. or credentials, its own email or LinkedIn, its own profile page, a job title in structured data) before one that only its page vouches for; when the limit would leave out every person who earns the People charge although one passes `personnelMinConfidence`, that person is kept first. So a small limit keeps the clearest people. |
| `personnelMinConfidence` | select | low | Drop contacts below this tier (`low`/`medium`/`high`). |
| `onlyWithPersonnel` | boolean | false | Keep only businesses with at least one named contact at or above `personnelMinConfidence` (implies `includePersonnel`; billed as People / contacts only). A contact with only an email and no name does not count. A card with only a name (no role, no contact) is at most low confidence, so with `high` (or `medium`) it can leave a business without a contact, and with `low` it keeps the business but creates no People charge. A named person that nothing proves (see below) also creates no People charge. |

People / contacts are billed once per saved business, and only when at least one person delivered for it is proven to be a real person. A person is proven in one of two ways. **Their own contact details:** their own email address (it spells their name: their first name, surname or both, a first initial with the surname, three or more initials such as `mls@`, or a nickname such as `bob@` for Robert), their own LinkedIn profile (the address spells their name), or a direct phone number printed in their own staff card. The business's Maps phone, an office, branch or toll-free line and a number shown on several people's cards are the business's, never a person's. **Or a role title inside a real staff list:** a job title (or a professional prefix such as Dr., Dra., Prof., or credentials such as DDS) for a person who is listed in a team or staff grid (two or more person cards side by side under a team or staff heading such as "Our Team", "Our Doctors" or "Our Leadership", in a section the site marks as its team, or each card with Dr., credentials, the person's own email or a link to their own page; one practitioner card with Dr. or credentials under such a heading counts too), on a staff or team page that lists two or more people (one card alone there counts only with the person's own contact details), on their own profile page in a staff directory (its address names the person and nothing else, apart from credentials such as `-dds`), or in the website's own structured-data record for that person (a schema.org Person that is only a person, is not the business itself, is not a blog or page author entry, is not an event performer, course instructor, article subject, product brand or page reviewer, and states the role). Inside a staff list, clinical and trade roles such as Orthodontist, Chiropractor, Veterinarian, Pharmacist, Groomer or Master Plumber count as role titles, and so does Dr. on its own. Whether a first name is common or unusual plays no part: `Chloe Martin`, `Siobhan Murphy` or `Aoife Byrne` in a team grid are billed like anyone else. Nothing else creates the charge: names taken from page headings (a page title or main heading such as "Dayton Family Dentist", also when it starts with a first name), one-off text in a paragraph, a footer or a menu, client testimonials and reviews ("What our clients say", or any card that quotes a client before their name), cards of products, classes, colours, software, awards, brands or packages ("Vinyasa Flow" / "Lead Instructor", "Agreeable Gray" / "Designer Favorite"), office and service-area cards ("Warner Robins" over its street address, "Morgan Hill" / "Family Dentist near Morgan Hill", a card whose role line names somebody else such as "Lead Dentist: Dr. Amy Park"), department cards ("Guest Experience", "Patient Care", "Human Resources"), blog or page author entries, the business's own structured-data record (also when the business is named after a person, such as "Tony Lopez Photography", or the record is typed both Person and Organization), an institution or education line ("Baylor University"), a practice-area or service heading split into a name and a title ("Atlanta Teeth" / "Whitening Specialist", "Teeth Whitening" / "Dentist"), a business name, and a name that was only guessed from sitting next to an email or phone number. Emails and social profiles found while looking for people are still returned in the row, but they are billed as Emails or Social profiles only when you turn on `includeEmails` / `includeSocials` (or `emailOnly` / `socialOnly`) yourself.

Each business gains `business_lead` (org-level email/phone/socials) and `contacts[]` (people with `name`, `title`, `email`, `phone`, `linkedin`, `confidence`, `tier`, `attributed`, `source_urls`). A person's fields carry only their own details: the business's phone, email and social profiles (its Maps phone, the site's header, footer and structured-data contacts, office mailboxes such as `info@`, toll-free numbers, and anything shown on several people's cards) stay in `business_lead` and are never put on a person, and a person found without own contact details has empty personal contact fields. Names come from team cards and staff or provider grids, staff lists whose items link to each person's own page, captions such as `Name | Role`, a name heading with its role on the next line on a staff-directory page (two or more needed), and structured data, and never from a page footer, a legal or licence line, a menu, a list of links, a button, a call to action, a client testimonial or a one-off line of text; a name in a footer counts only next to that person's own mailbox (a solo agent's name, role and mailbox). A person keeps a mailbox only when it is theirs (it spells their name, or it is a personal mailbox printed on their own card and on no other person's), and a branch or office phone on a location card goes to `business_lead`, and so does a billing, insurance, fax, front-desk, main, office or toll-free line printed in a person's card or block (a line labelled direct, mobile or cell stays theirs); when a person's card holds two unlabelled numbers, neither is put on the person. A role line is read without its label ("Title: Lead Dentist" gives "Lead Dentist"), and a line under another label ("Phone: ...", "Insurance: ...") is not a role. `attributed` is `true` only for a proven person: someone with their own email or LinkedIn, or someone vouched for by a team grid, a staff page, their own profile page or the site's structured-data record for that person; a professional prefix on the name (Dr., Dra., Prof.) counts as the role, a courtesy title (Mr., Ms.) does not. `attributed` is `false`, and the contact is low confidence and not billed, for a card with only a name (no role, no contact), a name guessed from an email address, a name that merely sat next to an email or phone number that is not theirs, a name from a page heading or a single paragraph, a page or article author named only in the page's metadata, and a person known only from a structured-data record typed as a business (a lawyer typed Person and Attorney with no email or LinkedIn of their own and no place in a team list). Headings, labels, navigation items, street names and email or web addresses are not returned as names (a widget or page label such as "Close Estimated Monthly Payment" or "FAX Attention" is dropped, or kept without a name when it carries an email). Institutions and education lines ("Baylor University") and practice-area and service headings split into a name and a title ("Atlanta Teeth" / "Whitening Specialist", "Teeth Whitening" / "Dentist"), also ones whose name opens with a city, are not returned as people either: a page's title or main heading is never a source of names, and neither are words cut from a longer heading ("Dayton Family" of "Dayton Family Dentist"), whatever the first word is. The business's own structured-data record is the business, not a person: a record named like the business or the site (also when the business is named after a person, when the Maps name adds a city such as "Paul Davis Restoration of Austin", or when the name ends with a trade or company word such as Restoration, Photography, Plumbing or Group), or typed both Person and Organization, gives its phone, email and profiles to `business_lead` unless it carries the person's own email or LinkedIn; a one-person site's record with a job title and that email or LinkedIn is that person. A lone first name with no role, email, phone or LinkedIn is never billed (it is left out, or returned as a low-confidence, unattributed hint). Precision comes before recall for billing: some real staff on pages without a proper staff list are returned but not billed; the business is still billed when another person on the site is proven. With `maxContactsPerBusiness`, contacts are ordered by confidence tier; within a tier, people who earn the People charge come first, then the person the business is named after, then those with personal-name evidence of their own (Dr. or credentials, own email or LinkedIn, own profile page, a structured-data Person with a job title), then those with a common given name (a hint for the order only, never for billing), then those vouched for only by their page; when the limit would leave out every person who earns the People charge although one passes `personnelMinConfidence`, one of them is kept first. So a small limit keeps the clearest people.

#### Ad intelligence (experimental add-on)

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `includeMetaAds` | boolean | false | Check the Meta Ad Library — is the business running Facebook/Instagram ads? With sample creatives. |
| `includeGoogleAds` | boolean | false | Check the Google Ads Transparency Center — is it running Search/YouTube/Display ads? |
| `onlyRunningAds` | boolean | false | Keep only businesses currently running ads. With neither ad option ticked it checks **both** Meta and Google and bills each completed check on the businesses it saves; with `includeMetaAds` or `includeGoogleAds` it checks and bills only the selected source(s). |
| `adCountry` | string | US | Two-letter country code for where the ads are shown (applies to both sources). `UK` is treated as `GB`. |

**Whose ads are reported.** A `null` ad result means "could not be tied to this business" and is never billed.

- **Meta:** the advertiser must be the business's Facebook page (its id or page address, found on the business's website or Maps website) or an exact page-name match that the business's own website or Facebook link backs up. A search that returns no ads is `meta_ads_running: null`, not `false`; `false` means the advertiser was identified and has no active ads. A sample whose headline is an unrendered catalog template (such as `{{product.name}}`) reads "No title available for this ad".
- **Google:** the Transparency Center lists every advertiser whose ads link to a website domain, agencies and retail-media brands included. The advertiser is used when its name equals the business name, or when a distinctive word of its name (legal and generic words such as Inc, LLC, Holdings, Brands, Media, Marketing and Dental do not count) appears in the place name or resembles the website's domain name; legal names such as "Target Brands, Inc", "Best Buy Purchasing LLC" and "Toothbar, PLLC" qualify. An advertiser whose name does not match is still used when it is the only advertiser on the domain (two accounts with the same name count as one), Google's first page shows at least half of the domain's ads, and it is not a known agency, retail-media brand or ordering/booking platform: this attributes owner-run personal accounts and parent companies. A lone agency, retail-media brand or ordering-platform tenant stays `null` and is not billed, and so does a parent company or personal name that shares the domain with another advertiser or sits on a large retailer or marketplace domain, even when it is the real advertiser. Businesses whose website is a site-builder or online-ordering page (`wixsite.com`, `square.site`, `menufy.com`, `chownow.com` and similar) get no Google lookup.
- **Google fields:** once an advertiser is accepted, `google_ads_advertiser`, `google_ads_advertiser_id` and `google_ads_samples` come from that advertiser (which can differ from the previous version, because that named the advertiser with the most ads on the page). As in the previous version, `google_ads_running`, `google_ads_last_shown`, `google_ads_formats` and `google_ads_active_count` cover every ad on the first page Google returns for the business's website domain, including ads the business shows through a second account or an agency. `google_ads_running` is true when one was shown in the past 30 days. `google_ads_active_count` is a minimum: on a single-page answer it counts the ads shown in the past 30 days (`0` when none was; the previous version counted every ad on the page, including old ones, so the number can be lower), and when Google reports more pages and ads are running it is the number of ads on the first page, as before.

#### Proxy

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `proxyConfiguration` | object | Apify Residential | Search and blocked full-detail recovery use the production-compatible Residential proxy group by default. |
| `proxyUrl` | string | — | Advanced: a single custom HTTP proxy used instead of Apify Proxy. It is tested before the run starts with up to 3 attempts, each on a fresh connection, so one dropped connection does not stop the run. The run stops with a message and collects nothing when the proxy rejects its credentials (HTTP 407) or every attempt fails the same way. |

***

### Recipes

**Web-design leads (businesses with no website):**

```json
{ "query": "restaurants in Austin, TX", "onlyWithoutWebsite": true, "includePersonnel": true }
```

**Warm leads — businesses advertising right now, with their contacts:**

```json
{ "query": "dentists in Miami, FL", "includeMetaAds": true, "includeGoogleAds": true,
  "onlyRunningAds": true, "includeEmails": true, "includePersonnel": true }
```

**Reputation / review mining (5★ reviews mentioning a topic):**

```json
{ "query": "hotels in Paris", "includeReviews": true, "reviewsLimit": 50,
  "minReviewRating": 5, "reviewKeyword": "breakfast" }
```

**Whole-area sweep with a custom radius:**

```json
{ "query": "gyms", "customGeolocation": { "lat": 34.0522, "lng": -118.2437, "radiusMeters": 5000 } }
```

***

### Tips

- **Include a location** in your query (`plumbers in Chicago`); plain phrases like `plumbers Chicago` or `coffee 90210` also work.
- **Keep Apify Proxy enabled** — the Actor uses the Residential proxy group by default for reliable search and detail retrieval.
- **Start with a small `maxResults`** to test, then scale up.
- The optional enrichment / ad-intelligence layers add run time and are **off by default** — they never change the base scrape.

### Limitations

- Results depend on what Google Maps returns for your query and location; some businesses have incomplete data.
- **Reviews come from Google's review list.** Reviews are read from Google's own review list over HTTP. With a rating or keyword filter, at most the first 2,000 reviews of each place are scanned; if that scan limit is reached before `reviewsLimit` matching reviews are found, the status message says so. A place whose whole list holds fewer matching reviews than `reviewsLimit` simply returns them, with no message. A place that Google shows without any review is not asked for its list. If a place's list cannot be fetched after retries, its embedded reviews are returned and the status message says how many saved places were affected (a more important status, such as a spending-limit or timeout stop, takes its place). A list that stops part-way keeps the reviews fetched before that, and a run that reaches its timeout keeps the reviews it has. When the list cannot be fetched for several places in a row, the Actor pauses it for a minute at a time and returns the embedded reviews meanwhile, so a Google-side outage cannot stall the run.
- `popular_times` is experimental: Google's place data almost never carries it, so expect `null` for virtually all places.
- If Google does not return a business's full place details, even after retries, the saved row contains only the basic data from the search result (for example name, address, coordinates, rating and category) and the detail-only fields such as opening hours, amenities and photos are empty. When `maxResults` is set, such a row is replaced by another business whenever one is available; otherwise it is saved and billed like any other result. A place you request by URL is never dropped. The run's status message then says how many saved rows have basic data only.
- The Meta check returns `meta_ads_running: null`, not `false`, for a business whose advertiser page cannot be identified in the Ad Library (including a business that has never advertised, and a business that advertises but whose Facebook page cannot be found); `false` means the advertiser was identified and has no active ads. The Google check needs the business's own website domain and an advertiser whose name resembles the business or its domain (or that is the only, non-listed advertiser on that domain); businesses with no website, a social profile, a directory listing, a site-builder or online-ordering page as their website get `null`, and so does a business whose parent company or owner account shares its domain with another advertiser, or whose only advertiser is a known agency, retail-media brand or platform. A `null` ad result is never billed.
- A row made from a place link (`startUrls`) can carry a few review highlights in `reviews` even when `includeReviews` is off. They have no author and are never billed.
- The agency, retail-media and ordering-platform names that the Google check refuses are a finite list: an agency the Actor does not know that is the only advertiser on a small business's domain can still be attributed to that business.
- People are read best from team pages that show each person as a card (name, role, optional contact) and from structured data. A page that gives each person a long biography section instead of a repeated card layout can come back with no named person; the addresses found there are then returned without a name or at business level. A doctor listed only as `Name, MD` (no `Dr.` and no link to a profile page) is skipped, because a bare MD, MA, MS or DC can also be a US state or city label, and a name that appears only in a photo's alt text is not matched with the card's phone and social profiles. A business that lists its branches or clubs as cards with a mailbox each that spells the place (`saintjohn@`, a gym chain's location page) can still return place names as people and one People unit when a card shows neither an address with a state or ZIP code nor a "near / in / serving <place>" line: a place name cannot otherwise be told from a person's name. A real team grid with no team heading, no team section and nothing else on its cards that proves a person (Dr., credentials, own email or profile link) is returned unattributed and not billed, and one person card alone on a team page is returned unattributed or left out unless it carries the person's own contact details. A staff page that lists office pets or mascots with a title ("Chief Barking Officer") returns them as people and bills them like staff, because a pet's name cannot be told from a person's, and a person the site spells two ways (a first name or a title written differently on two pages) can appear twice. Precision comes before recall for the People charge: a real person who is named only in running text, a heading, a footer or a structured-data record typed as a business (a lawyer typed Person and Attorney with no email or LinkedIn of their own), and who is not in a team list, is returned unattributed or left out and is not billed; the business is still billed when another person on the site is proven.
- A store page on a landlord's, mall's or directory's site is recognised by its address (a folder such as `/merchant/`, `/tenant/`, `/stores/`, `/shop/` or `/directory/` before the business's page) on a site whose own name does not name the business. A landlord page without such a folder is treated as the business's own site and its mailboxes are billed; a business's own mailbox on a landlord's domain is returned but not billed.
- The social accounts of a booking, listing or ordering platform are never attributed to a business, also when the business is itself such a platform's own listing; the fields stay empty. A link to one post, photo, video or event of a Facebook page, Facebook's old `/pg/<page>/...` form and a page tab such as `menu`, `shop`, `jobs` or `offers` give that page's profile (`facebook.com/<page>`), billed once with it as one social profile; a profile link found elsewhere on the site is preferred, but a site that links only to someone else's post can return that other page as its Facebook. A Facebook link that names no page (`facebook.com/events/<id>`, `photo/?fbid=`, `permalink.php`, `fb.me/e/<code>`) is not returned. A tab on a YouTube vanity address (`youtube.com/<name>/videos`) gives `youtube.com/<name>` (1.11.31 returned it as written).
- `customGeolocation` is searched as a bounding box: a `MultiPolygon` becomes one box covering all its parts, a point and radius becomes a box around the point, and a geometry the Actor does not understand (for example `Circle`) is ignored with a warning, so rows can come from outside the shape you drew.
- The Apify run option `maxItems` is not enforced by the Actor; use `maxResults` and a maximum total charge to cap a run.
- A place link that holds only a `/g/` id needs its name and coordinates (the full URL from your browser's address bar) to be found. A requested place that cannot be fetched is skipped and named in the run's status message; one that Google answers only partly is returned with what was found and named as incomplete. A place that the run timeout stopped before it could be looked up is not named as unfetchable: the status message reports the run timeout (run again with a longer timeout), and it does not list the links that were not reached. A link that really could not be fetched is still named when the timeout cut other links short; if the run saved no row for that reason it ends as failed with the run-timeout message, which names the lost link too.
- The Google Ads Transparency Center does not expose the text of search ads (only creative previews/metadata).
- For ad lookups outside the US, set `adCountry` to the business's country (e.g. `GB`, `DE`) for accurate results.

### Frequently asked questions

#### Do I need a Google Maps API key?

No. This is a scraper, not the Google Places API — there is no API key, no OAuth and no Google Cloud project. It talks to Google Maps' public endpoints over HTTP, so you just enter a search query and run it.

#### Can it scrape emails and contact details from Google Maps?

Yes, with the optional experimental add-ons. Turn on `includeEmails`, `includeSocials` or `includePersonnel` and the Actor visits each business's own website to find emails (MX-validated), social profiles and the people who work there. Off by default and billed separately (emails only on the business's own website domain or a free email provider); `includePersonnel` bills people/contacts only, not the emails and socials it returns unless you also turn those on.

#### Does it extract Google Maps reviews?

Yes. Enable `includeReviews` to receive up to `reviewsLimit` reviews per place (all of them when a place has fewer), newest first or in Google's relevant order, with author, rating, date, text, owner replies, reviewer statistics, language and photos, plus `minReviewRating` and `reviewKeyword` filters. Reviews are billed per delivered review that has an author, a rating and text or a date. They are an optional layer — the base place scrape runs without them.

#### Is it legal to scrape Google Maps?

The Actor reads only Google's public, unauthenticated listings — the same data any visitor sees. Scraping public data is broadly treated as lawful (e.g. hiQ v. LinkedIn), and Google's Terms are civil, not criminal. Use the output within applicable privacy laws.

#### How many businesses can I get from one search?

The Actor runs a grid search over the whole area, so it finds far more places than a single Google Maps page (which caps out quickly). Set `maxResults` to your target (0 = unlimited); when filters or unfetchable rows leave you short, the Actor looks at a bounded amount of extra places to fill the gap. Split very dense areas with a custom area or batch queries.

#### How is this different from the Google Places API?

No API key, no OAuth and no per-call quota. Each saved place has a base result price; optional reviews, emails, socials, personnel and completed ad checks have the separate unit prices shown above. The grid search returns more places than the API's per-search cap. You also get optional reviews, emails, contacts and ad intelligence the API does not provide.

#### Can I scrape a custom area, radius or Google Maps URL?

Yes. Target with a single `query`, a batch of `searchQueries`, direct Google Maps `startUrls`, or a `customGeolocation` (point + radius, or a GeoJSON polygon). Use whichever fits — you can leave `query` empty with `searchQueries` or `startUrls`, but a `customGeolocation` needs `query` for the category to search.

#### Can I find businesses without a website, or ones running ads?

Yes — both are built for agencies. Use `onlyWithoutWebsite` to find web-design prospects, and the ad-intelligence add-ons (`includeMetaAds`, `includeGoogleAds`, `onlyRunningAds`) to keep only businesses currently advertising on Meta or Google. `onlyRunningAds` on its own runs, and bills, both ad checks for the businesses it saves.

#### What formats can I export to?

Every run saves to an Apify dataset you can download as CSV, Excel (XLSX), JSON, JSONL or HTML, or pull via the Apify API and the official Python / JavaScript clients — ready for spreadsheets, CRMs and data pipelines.

### Other Actors by buff_pineapple

- **[Google Reviews Scraper](https://apify.com/buff_pineapple/google-reviews-scraper)** — every Google Maps review for a place, with reviewer details and owner replies.
- **[Meta Ad Library Scraper](https://apify.com/buff_pineapple/meta-ad-library-scraper)** — spy on Facebook & Instagram ads (creatives, spend, landing pages), no login.
- **[Yelp Scraper](https://apify.com/buff_pineapple/yelp-scraper)** — Yelp business profiles and reviews.
- **[Trustpilot Reviews Scraper](https://apify.com/buff_pineapple/trustpilot-reviews-scraper)** — every Trustpilot review plus the company TrustScore.
- **[Shopify Store Leads Scraper](https://apify.com/buff_pineapple/shopify-store-leads-scraper)** — detect Shopify stores and pull product + contact leads.
- **[Zillow Listings Scraper](https://apify.com/buff_pineapple/zillow-listings-scraper)** — homes for sale, rent and sold with full details.
- **[TikTok Top Ads Scraper](https://apify.com/buff_pineapple/tiktok-top-ads-scraper)** — TikTok Creative Center top-ads intelligence.

### Privacy & anonymous usage information

This Actor records a small amount of **anonymous, aggregate usage information** so the maintainer can understand which features are used and keep the Actor reliable. At the end of a run it may record, in the run's own storage on the Apify platform: the run ID, build number, run status/outcome, how the run was started, the number of results collected / pushed / filtered / skipped, and which optional features (reviews, emails, socials, personnel, ad-intelligence, etc.) were enabled, plus non-personal numeric settings (such as the result limit, reviews limit, language, and ad country).

**Usage metadata recorded:** when Apify provides an account ID, the Actor stores a truncated salted pseudonymous hash so distinct runs can be counted. It also stores a paying-status flag and per-feature counters. The salt is present in the source, so this hash should not be treated as cryptographic anonymization. No search query, geolocation/targeting input, scraped business data, contact details, raw account ID, IP address, or user-agent is stored in this record. Metadata stays in Apify key-value storage and is not sent to an external service. Recording is best-effort and fail-open; a storage failure does not stop or slow the run.

**Credentials.** No Google login is used and nothing is stored for reviews. Run logs hide proxy passwords and API tokens.

# Actor input Schema

## `query` (type: `string`):

What to search for on Google Maps — exactly what you'd type in the search box. Works best as "<category> in <location>", e.g. "restaurants in New York" or "dentists in Miami". Plain phrases like "coffee shops Seattle" or "plumbers 90210" also work — the location is detected automatically. Required unless you use Batch Search Queries or Google Maps URLs. A Custom Area also needs this field, because it supplies the category to search for. API callers must provide it explicitly; the prefill only fills the Console form.

## `searchQueries` (type: `array`):

Run several searches in one go — one query per line. Each is run through the full pipeline and results are merged and de-duplicated by place. Example: \["dentists in Miami", "orthodontists in Miami", "dental clinics Fort Lauderdale"]. Leave empty to use the single Search Query above.

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

Scrape from Google Maps URLs. A PLACE link fetches that single business with its full details: the full URL from the browser's address bar (with or without @lat,lng), a maps.app.goo.gl share link, a ?cid= link, or a ?q=place_id: / query_place_id link (a link with only a /g/ id needs its name and coordinates). A SEARCH url (e.g. .../maps/search/...) re-runs that query. A place that could not be fetched, or came back incomplete, is named in the run's status message; URLs that are not Google Maps links are skipped with a warning. Accepts plain URLs or {"url": "..."} objects.

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

Search an EXACT area without geocoding a place name. Provide either a point + radius: {"lat": 40.7128, "lng": -74.006, "radiusMeters": 3000}, or a GeoJSON geometry: {"type": "Polygon", "coordinates": \[\[\[lng,lat],...]]} (Polygon, MultiPolygon or Point supported). This sets WHERE to search, not WHAT to search: combine it with Search Query, which supplies the category. The area's bounding box is gridded directly.

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

Maximum number of businesses to extract. Set to 0 for unlimited, which collects everything found in the area and has no cost ceiling; use a small test run first and check its cost before scaling.

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

Two-letter language code for result text, e.g. en, es, fr, de, it, pt, ja.

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

Google Maps zoom level (1-21). Values 1-10 use the widest, coarsest bounded grid; values 11-15 progressively use a finer grid; values 16-21 use the finest bounded grid. 13 is a good default for city-level searches.

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

Fetch detailed info for each business (hours, phone, website, amenities). Slower but more complete.

## `includePhotos` (type: `boolean`):

Add a 'photos' array of up to 10 business photo URLs (from the place-details response). Requires 'Include Place Details' to be on.

## `includePlaceExtras` (type: `boolean`):

Add 'amenities' (attribute strings), 'plus_code' (Google Plus Code) and 'permanently_closed' (boolean). Best-effort — fields are null when not present in Google's response. Requires 'Include Place Details'.

## `includePopularTimes` (type: `boolean`):

EXPERIMENTAL / rarely available. Attempts to extract the weekly 'popular_times' busy histogram. Google's HTTP place endpoint almost never includes this data (it is normally loaded by a separate browser-only request), so in practice 'popular_times' is null for virtually all places. Kept for forward-compatibility; do not rely on it. Off by default. Requires 'Include Place Details'.

## `includeReviews` (type: `boolean`):

Fetch each business's Google reviews (the same list as the Reviews tab on Google Maps), with the author, rating, date, text, owner reply, reviewer details and photos. Each delivered review that has an author is billed separately; text-only highlights without an author are never billed. Increases run time and cost.

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

Maximum number of reviews to fetch per business (only used when Include Reviews is enabled). The actor pages through the business's review list until this many reviews are collected (counting only reviews that pass the rating and keyword filters, if set) or the list ends. If a business's full list cannot be fetched, the run's status message says so.

## `minReviewRating` (type: `integer`):

Keep only reviews rated at or above this many stars (1-5). 0 = keep all ratings (off). Reviews with no rating are always kept. Applied while paging, so 'Reviews per Business' counts matching reviews; with a rating or keyword filter on, at most the first 2,000 reviews of each business are scanned (if that limit is reached before enough reviews match, the run's status message says so).

## `reviewKeyword` (type: `string`):

Keep only reviews whose text contains this phrase (case-insensitive substring). Leave empty to keep all reviews. Example: 'refund', 'rude', 'clean'. Applied while paging, so 'Reviews per Business' counts matching reviews; with a rating or keyword filter on, at most the first 2,000 reviews of each business are scanned (if that limit is reached before enough reviews match, the run's status message says so).

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

Review ordering: 'Newest first' returns the most recent reviews first; 'Most relevant' uses Google's default order (the order shown first on Google Maps).

## `includeEmails` (type: `boolean`):

EXPERIMENTAL / BETA. Optional lead-enrichment add-on. Visits each business website (plus its contact / about / imprint pages) and extracts email addresses. Adds run time. Billed separately for each distinct delivered address on the business's own website domain or on a free email provider (such as Gmail, Outlook or Yahoo); other addresses found on its pages (for example a web agency's, a vendor's or another location's mailbox) are still returned but not billed. A business whose Maps website is a social profile, directory, site-builder, booking or ordering page, or a store page on a shopping centre's or landlord's site, has no domain of its own, so only its free-provider addresses are billed. Off by default — the base scraper is unaffected.

## `includeSocials` (type: `boolean`):

EXPERIMENTAL / BETA. Optional lead-enrichment add-on. Extracts social profile links (Facebook, Instagram, LinkedIn, X/Twitter, YouTube, TikTok, WhatsApp) from each business website. Adds run time and is billed separately for each distinct delivered profile URL. The official accounts of website builders and of booking, listing and ordering platforms (for example Wix or Booksy) found in page templates and footers are not returned as the business's. A link to one of a profile's tabs is returned as the profile itself, and so is a Facebook link to a post, photo, video or event of a page (its page profile); a link to a single Instagram post or reel, X post, TikTok or YouTube video is not a profile, and neither is a WhatsApp share button. Off by default.

## `emailOnly` (type: `boolean`):

Filter: only output businesses for which at least one email address was found. Automatically enables email finding, which is billed for each distinct delivered address on the business's own website domain or on a free email provider; other addresses are returned but not billed.

## `socialOnly` (type: `boolean`):

Filter: only output businesses for which at least one social profile was found. Automatically enables social profile finding, which is billed for each distinct delivered profile URL. A profile that is itself the business's Google Maps website, also a link to one of its tabs or posts that names the account, counts and is not billed.

## `onlyWithWebsite` (type: `boolean`):

Filter: only output businesses that have a real website (excludes those with only a social page, a directory listing, or no site at all).

## `onlyWithoutWebsite` (type: `boolean`):

Filter: only output businesses that do NOT have a real website — prime prospects for web-design and digital agencies.

## `maxPagesPerSite` (type: `integer`):

Advanced: max pages to crawl on each business website during lead/people enrichment (home + contact/about/team pages). Higher finds more contacts but is slower. Defaults to 4 (auto-raised to 6 when 'Find People / Contacts' is on).

## `includePersonnel` (type: `boolean`):

EXPERIMENTAL / BETA. Optional lead-enrichment add-on. Visits each business website and extracts the PEOPLE who work there — name, title, and their own email / phone / LinkedIn where published — as a 'contacts' list, plus a structured 'business_lead'. Only the business's own site is used; names are not taken from page footers, menus, lists of links or buttons, and the business's own phone, email and social profiles stay in 'business_lead' and are never shown as a person's. Billed once per delivered business with at least one proven person: someone with their own email, LinkedIn or direct phone, or someone with a role title (or Dr. / Prof. or credentials) in a real staff list (a team grid, a staff page, their own profile page or the site's structured-data record for that person). Names from page headings, single paragraphs, testimonials or the business's own record are never billed; institutions, practice-area headings and business names are not people. Product, class, award, brand, office, service-area and department cards are not staff either. Any emails or social profiles found on the way are still returned in the row, but they are billed only if 'Find Email Addresses' or 'Find Social Media Profiles' is turned on. Off by default.

## `maxContactsPerBusiness` (type: `integer`):

Maximum personnel contacts to output per business (highest-confidence first; within a confidence tier, people who earn the People charge come first, then people with their own personal-name evidence such as Dr., credentials or their own email; one person who earns the charge is always kept when any passes the confidence floor). Only used when 'Find People / Contacts' is enabled.

## `personnelMinConfidence` (type: `string`):

Drop personnel contacts below this confidence tier. 'low' keeps everything (recall-first); raise to 'medium' or 'high' for precision. Only used when 'Find People / Contacts' is enabled.

## `onlyWithPersonnel` (type: `boolean`):

Filter: only output businesses for which at least one named person was found. Automatically enables 'Find People / Contacts', billed once per delivered business with at least one proven person (their own email, LinkedIn or direct phone, or a role title in a real staff list); emails and social profiles found on the way are not billed.

## `includeMetaAds` (type: `boolean`):

EXPERIMENTAL / BETA. Ad-intelligence add-on. Checks the public Meta Ad Library (https://www.facebook.com/ads/library) for each business and reports whether they are actively running ads on Facebook / Instagram — plus the active-ad count and a few sample creatives (call-to-action, landing page, ad copy). A strong buy signal for agencies. Matches conservatively on the business's Facebook page, or on an exact page name that is backed by the business's own website or Facebook link, so unrelated advertisers are not falsely attributed. Adds run time, uses residential proxy, billed separately for each business whose own advertiser is identified (including one with no active ads). A search that finds nothing, or only ads that cannot be attributed to the business, returns null and is not billed. Off by default.

## `includeGoogleAds` (type: `boolean`):

EXPERIMENTAL / BETA. Ad-intelligence add-on. Checks the public Google Ads Transparency Center (https://adstransparency.google.com) for each business website and reports whether they are actively running ads on Google — Search, YouTube and Display — plus the advertiser name, ad count, when ads were last shown, and sample creatives. Looked up on the business's own website domain. The advertiser is attributed when its name matches the business or its domain, or when it is the only advertiser on that domain and is not a known agency, retail-media brand or ordering/booking platform (this covers owner-run personal and parent-company accounts). Ads that cannot be attributed (for example an agency, a brand selling through a retailer, an ordering-platform tenant, or several unrelated advertisers sharing the domain) return null and are not billed. Adds run time, billed separately for each completed website-backed business check whose advertiser is attributed to the business, including a verified no-match. Off by default.

## `onlyRunningAds` (type: `boolean`):

Filter: only output businesses that are actively running ads. If neither 'Find Meta Ads' nor 'Find Google Ads' is selected, both are checked; otherwise only the selected source(s). Keeps any business currently advertising on an enabled platform. The ad checks it runs are billed for each business it outputs, exactly as if you had selected them.

## `adCountry` (type: `string`):

Two-letter ISO country code for the ad lookups (where the ads are shown), e.g. US, GB, DE. Applies to both the Meta Ad Library search and the Google Ads Transparency Center region. Defaults to US. Used when an ad-intelligence add-on is enabled.

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

Required for reliable results. Google blocks datacenter IPs on Maps search, so this defaults to Apify Residential proxy. Leave as-is unless you know what you're doing.

## `proxyUrl` (type: `string`):

Optional. A single HTTP proxy URL (http://user:pass@host:port) to use INSTEAD of Apify Proxy above. Leave empty to use the Proxy configuration. The proxy is tested before the run starts, with up to 3 attempts on fresh connections; the run stops with a message, and collects nothing, only if the proxy rejects its credentials (HTTP 407) or every attempt fails the same way.

## Actor input object example

```json
{
  "query": "restaurants in New York",
  "searchQueries": [],
  "startUrls": [],
  "customGeolocation": {},
  "maxResults": 100,
  "language": "en",
  "zoom": 13,
  "includeDetails": true,
  "includePhotos": true,
  "includePlaceExtras": true,
  "includePopularTimes": false,
  "includeReviews": false,
  "reviewsLimit": 5,
  "minReviewRating": 0,
  "reviewKeyword": "",
  "reviewsSort": "newest",
  "includeEmails": false,
  "includeSocials": false,
  "emailOnly": false,
  "socialOnly": false,
  "onlyWithWebsite": false,
  "onlyWithoutWebsite": false,
  "includePersonnel": false,
  "maxContactsPerBusiness": 10,
  "personnelMinConfidence": "low",
  "onlyWithPersonnel": false,
  "includeMetaAds": false,
  "includeGoogleAds": false,
  "onlyRunningAds": false,
  "adCountry": "US",
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

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

One item per Google Maps place: name, full address, latitude/longitude, phone, website, rating, review count, category, price level, place ID/CID, plus any enabled enrichment (contacts, reviews, ad intelligence).

# 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 = {
    "query": "restaurants in New York",
    "searchQueries": [],
    "startUrls": [],
    "customGeolocation": {},
    "includePhotos": true,
    "includePlaceExtras": true,
    "includePopularTimes": false,
    "reviewsSort": "newest",
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("buff_pineapple/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 = {
    "query": "restaurants in New York",
    "searchQueries": [],
    "startUrls": [],
    "customGeolocation": {},
    "includePhotos": True,
    "includePlaceExtras": True,
    "includePopularTimes": False,
    "reviewsSort": "newest",
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("buff_pineapple/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 '{
  "query": "restaurants in New York",
  "searchQueries": [],
  "startUrls": [],
  "customGeolocation": {},
  "includePhotos": true,
  "includePlaceExtras": true,
  "includePopularTimes": false,
  "reviewsSort": "newest",
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call buff_pineapple/google-maps-scraper --silent --output-dataset

```

## MCP server setup

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