Arcadium Data Scrapers
Home / Tutorials / Local business leads from Google Maps

Google Maps scraper for local leads: every business in a city with phone, website and hours

Sales teams and agencies build local lead lists from Google Maps: every plumber in Denver, every dentist in Austin. One Google Maps search stops at about 200 places, and copying listings by hand does not scale.

arcadium/google-maps-places splits the area into map cells, searches each one and splits again where a cell is still full, so a city search goes past one result list. Each business is one JSON row: name, categories, full address, coordinates, rating and review count, the website and phone the business publishes on its listing, opening hours for the next 7 days and booking links. A free coverage row per search says how many cells were searched and why the search stopped.

Cost of this example on the Apify Free plan: 300 × $0.002 = $0.60 ($0.30 on the Apify Gold plan and higher), plus $0.001 per GB of memory when the run starts.

What you need

Step 1: Run it in the Apify Store (no code)

  1. Open arcadium/google-maps-places on the Apify Store and sign in (a free Apify account works).
  2. Type the business type into Search terms, for example plumber. Several terms share one location.
  3. Set Location to a city, district or postcode, for example Denver, CO.
  4. Set Max places per search to 300 (0 means every place the search finds) and turn on Skip closed places.
  5. Optional: set a maximum cost per run in the run options as a hard cap.
  6. Click Start. Rows appear in the Output tab; export them as JSON, CSV or Excel.

The same input as JSON (paste it in the JSON tab of the input form):

{
  "searchTerms": [
    "plumber"
  ],
  "location": "Denver, CO",
  "maxPlacesPerSearch": 300,
  "skipClosed": true
}

Step 2: Call it over the Apify API

One HTTP call runs the actor and returns its rows. It suits runs up to about five minutes; for longer runs start the run asynchronously and read the dataset when it finishes.

curl -X POST "https://api.apify.com/v2/acts/arcadium~google-maps-places/run-sync-get-dataset-items" \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"searchTerms":["plumber"],"location":"Denver, CO","maxPlacesPerSearch":300,"skipClosed":true}'

Step 3: Run it from Python

With apify-client 3.x, call() returns a Run object: read the dataset id as run.default_dataset_id (older examples use run["defaultDatasetId"], which no longer works).

# pip install "apify-client>=3"
import csv, os

from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("arcadium/google-maps-places").call(run_input={
    "searchTerms": ["plumber"],
    "location": "Denver, CO",
    "maxPlacesPerSearch": 300,
    "skipClosed": True,
})
rows = list(client.dataset(run.default_dataset_id).iterate_items())  # apify-client 3.x

keys = ["placeId", "name", "category", "address", "city", "postalCode", "phone",
        "website", "rating", "reviewCount", "latitude", "longitude", "placeUrl"]
with open("plumbers-denver.csv", "w", newline="") as f:
    w = csv.DictWriter(f, fieldnames=keys, extrasaction="ignore")
    w.writeheader()
    w.writerows(r for r in rows if r.get("rowType") == "place")
for c in (r for r in rows if r.get("rowType") == "coverage"):
    print("delivered", c.get("delivered"), "stop", c.get("stopReason"), "full cells", c.get("cellsSaturatedAtMin"))

Step 4: Run it from JavaScript

// npm install apify-client
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('arcadium/google-maps-places').call({
  "searchTerms": [
    "plumber"
  ],
  "location": "Denver, CO",
  "maxPlacesPerSearch": 300,
  "skipClosed": true
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items);

Step 5: Ask an AI assistant (MCP)

Apify’s hosted MCP server turns the actor into one tool. Add it to Claude, Cursor, VS Code or any MCP client:

{
  "mcpServers": {
    "apify": {
      "url": "https://mcp.apify.com/?tools=arcadium/google-maps-places",
      "headers": {
        "Authorization": "Bearer <APIFY_TOKEN>"
      }
    }
  }
}

Then ask, for example:

Use arcadium/google-maps-places to list plumbers in Denver, CO with a rating of 4.5 or more. Give me name, phone, website and review count, sorted by review count.

More clients and the local setup on the MCP page.

Businesses without a website

Web agencies look for local businesses that have no website on their listing. Set website to without (or with for the opposite). Filters run on the places Google returns, so you pay only for the places that pass.

{
  "searchTerms": [
    "restaurant"
  ],
  "location": "Austin, TX",
  "website": "without",
  "maxPlacesPerSearch": 0
}

Check what the search covered

Each search ends with a free coverage row: delivered places, cellsSearched, cellsSaturatedAtMin (cells still full at the smallest size, where places may be missing; lower minCellKm to go deeper) and stopReason (end when the area is exhausted, limit when your maximum was reached, budget when the page budget or the run’s maximum charge stopped it).

Your own area: a radius or a polygon

Instead of a place name, pass latitude, longitude and radiusKm for a circle, boundingBox, or a GeoJSON polygon. Add minRating and categoryFilter to keep only the places you want. Re-run monthly and compare on placeId, which stays the same between runs.

Output fields

Every row has a rowType; filter on it first. Failed inputs come back as free rows with a code and message. The fields this tutorial uses, from the actor’s dataset schema:

FieldWhat it holds
rowTypeKind of row. Filter on this first. 'place': one business (charged). 'coverage': one free row per search with places delivered, map cells searched and why it stopped. 'error': an input could not be read (free).
placeIdGoogle place id (ChIJ...). Stable; use it to deduplicate between runs and with the Google Places API.
nameBusiness name as Google Maps shows it.
categoryMain Google category ('Dentist').
addressFull address as Google prints it.
cityCity or town of the address.
postalCodePostal code.
latitudeLatitude (WGS84).
longitudeLongitude (WGS84).
ratingGoogle star rating, 1-5.
reviewCountNumber of Google reviews.
websiteWebsite the business lists on Google.
phoneBusiness phone as printed in its country, as published on the Google listing; null when includePhones is off or the listing has none.
openingHoursOpening hours of the next 7 days: [{day, date, hours: ['8 AM–5 PM']}] (holiday-aware, dated).
closedTrue when Google marks the place closed (permanently or temporarily).
deliveredCoverage rows: places delivered for the search.
cellsSaturatedAtMinCoverage rows: cells that still had 100+ places at the smallest cell size (some places there may be missing).
stopReasonCoverage rows: why the search ended (limit = max places reached, end = area exhausted, budget = run charge limit, error).

All fields: Google Maps Places Scraper guide.

Cost per run

Pay per event: you pay for the rows delivered, at the actor’s live Apify price on 2026-11-03. Error, coverage and summary rows are free.

WhatRowsPrice per row (Free plan)Subtotal
Place: 1 search term, up to 300 places300$0.002$0.60

Cost of this example on the Apify Free plan: 300 × $0.002 = $0.60 ($0.30 on the Apify Gold plan and higher), plus $0.001 per GB of memory when the run starts. Higher Apify plans pay less per row. The Store page always has the current price.

Limitations

Links