# Google Maps Contact Scraper (`lightmoon/google-maps-contact-scraper`) Actor

Search Google Maps by category and city to find local businesses, and export phone numbers, websites and addresses to CSV or Excel. 46 columns each: split address fields, opening hours, rating, and flags for no website or an unclaimed profile. Build cold calling and lead lists.

- **URL**: https://apify.com/lightmoon/google-maps-contact-scraper.md
- **Developed by:** [Stable](https://apify.com/lightmoon) (community)
- **Categories:** Lead generation, Business, Automation
- **Stats:** 2 total users, 1 monthly users, 66.7% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.80 / 1,000 business phone numbers

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

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

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

Type a category and a city. Get back every local business Google Maps lists for
it, with the phone number, the website, the address split into fields, and the
opening hours — 46 columns, straight to CSV, Excel, JSON or your CRM.

```json
{ "searchTerms": ["dentist"], "locations": ["Austin, TX"] }
```

No Google API key, no daily quota, no browser.

### What one row looks like

Copied from a real run in August 2026. Nothing here is typed by hand.

```json
{
  "name": "Toothbar",
  "categoryName": "Dentist",
  "categories": ["Dentist", "Cosmetic dentist", "Dental implants provider", "Emergency dental service"],
  "phone": "(512) 607-4268",
  "phoneUnformatted": "+15126074268",
  "website": "https://www.toothbar.com/",
  "domain": "toothbar.com",
  "bookingUrl": "https://www.toothbar.com/contact/",
  "hasPhone": true,
  "hasWebsite": true,
  "claimed": true,
  "address": "211 Walter Seaholm Dr LR 160, Austin, TX 78701",
  "street": "211 Walter Seaholm Dr LR 160",
  "city": "Austin",
  "state": "Texas",
  "stateCode": "TX",
  "postalCode": "78701",
  "countryCode": "US",
  "neighborhood": "Seaholm District",
  "latitude": 30.2671177,
  "longitude": -97.7527535,
  "distanceFromCenterKm": 0.93,
  "rating": 4.9,
  "reviewsCount": null,
  "description": "Downtown spot specializing in cosmetic dentistry such as veneers and dental checkups, and offering fillings.",
  "openNow": false,
  "currentStatusText": "Closed · Opens 8 AM Mon",
  "openingHours": [{ "day": "Sunday", "hours": "Closed" }],
  "timezone": "America/Chicago",
  "permanentlyClosed": false,
  "temporarilyClosed": false,
  "serviceAreaBusiness": false,
  "attributes": ["Wheelchair accessible entrance", "Wheelchair accessible parking lot", "Wheelchair accessible restroom"],
  "highlights": ["Identifies as women-owned"],
  "placeId": "ChIJO0xN0RG1RIYRIDFDZYJkYjA",
  "cid": "6448317149061214008",
  "googleMapsUrl": "https://www.google.com/maps/place/?q=place_id:ChIJO0xN0RG1RIYRIDFDZYJkYjA",
  "rank": 17,
  "searchTerm": "dentist",
  "searchLocation": "Austin, TX",
  "searchQuery": "dentist Austin, TX",
  "scrapedAt": "2026-08-16T23:45:06Z"
}
```

### How often each field is actually filled

Measured across **260 businesses** — five categories in five cities, on
17 August 2026. These are the numbers to plan a campaign against, and they are
published because a lead list is only as good as the columns you can act on.

| Column | Filled |
|---|---|
| Name, category, coordinates, rating, Google Maps link, place ID | **100%** |
| **A phone number, a website, or both** | **100%** |
| **Website** | **97%** |
| **Phone number** | **93%** |
| Address, split into street / city / state / postcode / country | 97% |
| Opening hours (at least today's) | 99.6% |
| Editorial description | 40% |
| Accessibility and service attributes | 77% |
| **Review count** | **28%** — see Limits |

### Fields

46 columns on every row, in a fixed order, so the CSV is never ragged.

| Field | What it is |
|---|---|
| `name` | Business name |
| `categoryName` · `categories` | Primary category, and every category Google lists |
| `phone` · `phoneUnformatted` | As Google prints it, and in +E.164 form for a dialer |
| `website` · `domain` | Published website with Google's tracking parameters stripped, and the bare domain |
| `bookingUrl` | Appointment or ordering link, where the business publishes one |
| `hasPhone` · `hasWebsite` | Booleans, for filtering in a spreadsheet |
| `claimed` | `false` when nobody has claimed the Google Business Profile |
| `address` | The printed one-line address |
| `street` · `city` · `state` · `stateCode` · `postalCode` · `countryCode` | The same address split into fields, for a mail merge |
| `neighborhood` · `plusCode` | District, and the Open Location Code when there is no street address |
| `latitude` · `longitude` · `distanceFromCenterKm` | Position, and how far it is from the place you searched |
| `rating` · `reviewsCount` | Stars out of five, and the review count when Google returns it |
| `description` | Google's own editorial summary of the business |
| `openNow` · `currentStatusText` · `openingHours` · `timezone` | Open right now, the status line, the week, and the local time zone |
| `permanentlyClosed` · `temporarilyClosed` | Closure flags |
| `serviceAreaBusiness` | `true` for a trade that serves an area and keeps no storefront — these have no street address by design |
| `attributes` · `highlights` | Wheelchair access, service options, and owner-identity badges |
| `thumbnailUrl` | The listing photo |
| `placeId` · `cid` · `featureId` · `googleMapsUrl` | Google's own identifiers, and a link that opens the exact business |
| `rank` | Position in Google's result list |
| `searchTerm` · `searchLocation` · `searchQuery` · `searchResolvedLocation` | Which of your searches found it, and where Google decided to look |
| `scrapedAt` | UTC timestamp |

### Filters

Every filter runs **before** a row is stored or billed, so a business you filter
out costs you nothing.

| Input | What it does |
|---|---|
| `onlyWithPhone` | Drop businesses with no phone number |
| `websiteFilter` | `Only businesses with no website` — the classic local-agency lead list |
| `onlyUnclaimed` | Only profiles nobody has claimed |
| `minRating` | Star floor |
| `skipClosedBusinesses` | Drop permanently and temporarily closed businesses |
| `language` · `countryCode` | Language of the opening-hours text, and which Google edition to search |

### Recipes

**Cold-call list for one trade in one city.** `searchTerms: ["plumber"]`,
`locations: ["Chicago, IL"]`, `onlyWithPhone: true`.

**Businesses with nowhere to send customers.** `websiteFilter:
"withoutWebsite"` — on a measured run, 11 of those 38 businesses also had an
unclaimed profile.

**Profiles nobody is managing.** `onlyUnclaimed: true`. About 2 businesses in
100, and the clearest signal that a local business is not looking after its own
listing.

**A whole region, one run.** Three categories across four towns is twelve
searches: `searchTerms: ["dentist", "orthodontist", "dental clinic"]`,
`locations: ["Austin, TX", "Round Rock, TX", "Georgetown, TX", "San Marcos, TX"]`.

### What it costs

Billed per business stored, at one of two rates, decided by what came back:

| Event | When | Share of a measured run |
|---|---|---|
| **Business phone number** | the row has a phone number | 94% |
| Business listing | the row has no phone number | 6% |

A business with no phone number is billed at the lower rate. Nothing is charged
for a row a filter removed, and one business found by three of your searches is
stored and billed once.

**One price covers the whole row** — the phone number, the website, the split
address and the opening hours are all included in the rate on the card. There is
no separate charge for contacts.

**Apify's free plan** includes $5 of platform credit a month, which is roughly
**1 100 businesses** at the free-plan rate. Paid plans pay the rate shown on the
card. There is no separate free trial — you are billed per row from the first
row, so start with `maxResults: 20` and look at the output before scaling up.

**Speed:** 300 businesses in **36 seconds** on the smallest machine Apify
offers, in 16 requests.

### Limits

Stated plainly, because you will meet all four.

**One search returns about 200 businesses, and then Google stops.** Measured:
offsets 0–180 each returned a full page, offset 200 returned 17, offset 220
returned nothing. Asking deeper buys nothing. **More coverage comes from asking
more questions** — and it genuinely works: one search for `dentist Austin, TX`
returned 20 businesses, while four narrower searches around the same city
returned **83 different ones**, 16 to 20 of every 20 unseen in the first. Split
by neighbourhood, by postcode, by suburb, or by a narrower category.

**Review counts arrive on about a third of responses.** The star rating comes
back every time; the number of reviews behind it is Google's choice, made per
response, and a repeat request recovers it only about one time in four. So
`reviewsCount` is `null` rather than `0` when it is missing — a zero would be a
lie you could sort on — and there is deliberately no minimum-reviews filter,
because it would silently drop three businesses in four.

**Opening hours are the full week on about a quarter of responses** and today's
row on the rest. Same decision by Google, same response: you always get
`openNow` and the status line.

**No e-mail addresses and no social profiles.** Google Maps does not publish
them; the listings that do sell them get them by visiting each company's own
website afterwards. This actor returns what Google itself publishes, which is
why it is fast and cheap. If you need e-mail, take the `website` column and
enrich it separately.

### Output

Five ready-made views on the dataset tab: **Call list**, **Lead signals**,
**Address fields**, **Opening hours**, and **All fields**. Export any of them to
CSV, Excel, JSON, XML or HTML, or pull them through the Apify API.

Every run also writes a `RUN_SUMMARY` record: how many businesses were stored,
how many carried a phone number, which searches returned nothing, which filter
dropped what, and how many searches ran into Google's own ceiling.

### FAQ

#### How much does it cost to scrape 10,000 businesses?

Ten thousand rows at the rate on the card, plus a few cents of Apify platform
usage — a measured 300-row run used $0.002 of it. Rows removed by a filter are
not billed.

#### Can I search a whole country?

Not in one search. Google caps a single search at about 200 businesses wherever
you point it. List the cities you want as separate locations; every category ×
location pair is its own search, and the results are de-duplicated by place ID
across all of them.

#### Does it work outside the United States?

Yes. The location is plain text, so `Camden, London`, `Kreuzberg, Berlin` or
`Shibuya, Tokyo` all work. Set `countryCode` and `language` to match the market
you are searching.

#### Why is the address empty on some rows?

Because the business has no storefront — a mobile locksmith, a plumber, a
groomer who comes to you. Google publishes a service area instead of an address
for these, and the row is flagged with `serviceAreaBusiness: true`. They still
carry a phone number.

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

No. Nothing here touches the Google Places API, so there are no daily quotas and
no billing account to set up.

#### Will one business appear twice?

No. Rows are de-duplicated by Google's own place ID across every search in the
run, so a plumber matched by both `plumber` and `drain cleaning` is stored once
and billed once.

#### How do I get more than 200 results for one category?

Narrow the location and repeat. Four searches around Austin returned 83 distinct
dentists where one returned 20. Postcodes, neighbourhoods and suburbs all work
as locations.

### Other scrapers we maintain

Same engine, same pricing model, same promise: flat rows, stated limits,
and no charge for a row that came back empty.

| | |
|---|---|
| **[Local Business Phone Scraper](https://apify.com/lightmoon/local-business-phone-scraper)** | The same job from directory sources, with Better Business Bureau accreditation on the row. |
| **[YouTube Influencer Scraper](https://apify.com/lightmoon/youtube-influencer-scraper)** | Creators by niche, with the website and socials they publish and a subscriber filter. |
| **[Telegram Channel Messages Scraper](https://apify.com/lightmoon/telegram-messages-scraper)** | Posts from public channels — text, views, reactions and media links, no account needed. |
| **[Threads Hashtag Scraper](https://apify.com/lightmoon/threads-hashtag-scraper)** | Threads posts by hashtag or keyword, with author, engagement and links. |
| **[RedNote Xiaohongshu Profile Scraper](https://apify.com/lightmoon/rednote-profile-scraper)** | Public RedNote creator profiles in bulk — followers, bio, IP location and their notes. |
| **[ATS Jobs Scraper](https://apify.com/lightmoon/ats-jobs-scraper)** | Open roles straight from company career pages on Greenhouse, Lever, Ashby and Workday. |

# Actor input Schema

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

Business categories or keywords, one per line — `dentist`, `plumber`, `coffee shop`. Combined with every location below, so 2 terms and 3 cities is 6 searches.

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

Cities, neighbourhoods or regions, one per line — `Austin, TX`, `Camden, London`, `Kreuzberg, Berlin`. Anywhere Google Maps understands works. Leave empty if your search terms already name the place, like `dentist austin tx`.

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

Across all searches. 0 means no limit. Free-plan runs are capped by your own Apify usage, so start small.

## `maxResultsPerQuery` (type: `integer`):

How deep to page into one search before moving to the next. Google stops serving a single search at roughly 200 businesses however deep you ask, so more coverage comes from adding categories and cities rather than raising this.

## `onlyWithPhone` (type: `boolean`):

93% of businesses measured published a phone number. Turn this on and the rest are skipped before they are stored or charged.

## `websiteFilter` (type: `string`):

`Businesses with no website` is the classic local-agency lead list: someone on Google Maps who has nowhere to send customers. 2.7% of businesses measured had no website.

## `onlyUnclaimed` (type: `boolean`):

A profile the owner has never claimed — Google still offers a `Claim this business` link on it. The clearest sign a local business is not managing its own listing. 1.9% of businesses measured were unclaimed, so expect a short list.

## `minRating` (type: `integer`):

0 keeps everything. Businesses Google shows no rating for are skipped when this is above 0.

## `skipClosedBusinesses` (type: `boolean`):

Drops anything Google marks permanently or temporarily closed. None appeared in the 260 businesses measured, so this rarely changes a run — it is here so a closed lead is never delivered silently.

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

Two-letter code. Decides the language of the opening-hours and status text Google returns.

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

Two-letter code for the Google country edition to search from — `us`, `uk`, `de`, `au`. Affects how ambiguous place names are resolved.

## Actor input object example

```json
{
  "searchTerms": [
    "dentist"
  ],
  "locations": [
    "Austin, TX",
    "Dallas, TX"
  ],
  "maxResults": 200,
  "maxResultsPerQuery": 200,
  "onlyWithPhone": false,
  "websiteFilter": "any",
  "onlyUnclaimed": false,
  "minRating": 0,
  "skipClosedBusinesses": false,
  "language": "de",
  "countryCode": "de"
}
```

# Actor output Schema

## `callList` (type: `string`):

No description

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

No description

## `address` (type: `string`):

No description

## `hours` (type: `string`):

No description

## `all` (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": [
        "dentist",
        "orthodontist"
    ],
    "locations": [
        "Austin, TX"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("lightmoon/google-maps-contact-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": [
        "dentist",
        "orthodontist",
    ],
    "locations": ["Austin, TX"],
}

# Run the Actor and wait for it to finish
run = client.actor("lightmoon/google-maps-contact-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": [
    "dentist",
    "orthodontist"
  ],
  "locations": [
    "Austin, TX"
  ]
}' |
apify call lightmoon/google-maps-contact-scraper --silent --output-dataset

```

## MCP server setup

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