# Local Pack Rank Checker - Google Maps by City or ZIP (`steadyscrape/local-pack-rank-checker`) Actor

Local rank checker for Google Maps: where a business ranks for each keyword in each city, ZIP or postal code you list, its rank (top 20 or 40), the map pack (3-pack) and who holds #1 in every place, with a summary and the competitors that beat it. Local SEO data as CSV, JSON or API. No login.

- **URL**: https://apify.com/steadyscrape/local-pack-rank-checker.md
- **Developed by:** [Steady Scrape](https://apify.com/steadyscrape) (community)
- **Categories:** SEO tools, Marketing, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 location checks

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

## Local Pack Rank Checker — Google Maps rankings by city, ZIP or postal code

Check **where a business ranks on Google Maps for each keyword in every city, town, ZIP or postal code you list** —
the way a customer searching from each of those places sees it. For each keyword in each place you get the business's
**rank**, whether it is in the **local 3-pack**, **who holds #1 there** and the whole 3-pack with ratings. For each
keyword you get a **summary across all the places**: where it is found, where it is in the pack, its average rank, its
share of local voice and the competitors that beat it most often.

A pay-per-check local rank checker: no subscription, no login, no browser, no Google API key, no geocoding key — a
city or a ZIP is placed by Google Maps itself.

### What it is for

- **Service-area businesses** (plumbers, roofers, HVAC, cleaners, movers): one run tells you in which of the fifteen
  towns you serve you show up in the map pack, and who takes the customers where you do not.
- **Local SEO agencies**: a city-by-city or ZIP-by-ZIP map pack report per client and keyword, ready for a spreadsheet.
- **Multi-location brands and franchises**: one keyword, every market, one table.
- **Competitor tracking**: add up to nine competitors as extra businesses; one search ranks all of them, each with its
  own row and summary.
- **Before / after**: run the same list before and after a Google Business Profile change, a review push or a new page.

Need a grid around one location instead (a 7×7 heatmap of a neighbourhood)? Use our
[Google Maps Rank Tracker (Geo-Grid)](https://apify.com/steadyscrape/google-maps-rank-tracker).

### How it works

1. You give the business (its **Google Maps link**, a `maps.app.goo.gl` share link, its **place id** or its **CID**),
   one or more **keywords**, and the **locations**: city names (`Austin, TX`), ZIP or postal codes (`78704`,
   `LS1 4AP`, `M5V 2T6`) or coordinates (`30.2672, -97.7431`).
2. The Actor looks the business up on Google Maps, and places each location where Google Maps' first result for it
   lies (coordinates are used as given).
3. In every location it runs each keyword the way Google Maps does for someone looking at the map there (zoom 13 by
   default: part of a city), and finds the business **by its place id** among the results — never by its name, so a
   branch or a namesake is never mistaken for it.
4. It writes one row per business × keyword × location, one summary per business × keyword, and a free row for each
   input it could not place.

**Ranks 1–20 by default.** Google Maps lists twenty places a page. Turn on **Look down to rank 40** to read a second
page in the locations where the business is not in the first twenty.

### Input example

```json
{
    "businesses": ["https://www.google.com/maps/place/Swish+Dental+Downtown/data=!4m2!3m1!1s0x8644b5b434046875:0x644e6eb422388b7a"],
    "keywords": ["dentist", "cosmetic dentist"],
    "locations": ["Austin, TX", "78704", "Round Rock, TX", "30.2672, -97.7431"],
    "checkTop40": false,
    "zoom": 13,
    "language": "en",
    "country": "us"
}
```

### Output

Every row has a `rowType`: `check` (one keyword in one location for one business), `summary` (one keyword over all the
locations), `business` or `location` (an input that could not be used, free).

#### A check

```json
{
    "rowType": "check",
    "status": "ok",
    "businessName": "Swish Dental Downtown",
    "placeId": "ChIJdWgENLS1RIYReos4IrRuTmQ",
    "cid": "7227836171923524474",
    "keyword": "dentist",
    "location": "Austin, TX",
    "locationName": "Austin",
    "locationAddress": "Austin, TX",
    "locationCategory": null,
    "lat": 30.267153, "lng": -97.7430608,
    "rank": 1,
    "inLocalPack": true,
    "inTop20": true,
    "searchedDepth": 20,
    "resultsSeen": 20,
    "leaderName": "Swish Dental Downtown",
    "localPack": [
        { "position": 1, "name": "Swish Dental Downtown", "placeId": "ChIJdWgENLS1RIYReos4IrRuTmQ", "cid": "7227836171923524474", "rating": 4.5, "reviewCount": 992, "category": "Dentist" },
        { "position": 2, "name": "Breeze Dental", "placeId": "ChIJrTahjN-0RIYRchMKUA6urbg", "cid": "13307483850445296498", "rating": 4.8, "reviewCount": 1002, "category": "Dentist" },
        { "position": 3, "name": "Toothbar", "placeId": "ChIJO0xN0RG1RIYRIDFDZYJkYjA", "cid": "3486459572745613600", "rating": 4.9, "reviewCount": 735, "category": "Dentist" }
    ],
    "mapsUrl": "https://www.google.com/maps/search/dentist/@30.267153,-97.743061,13z"
}
```

| field | meaning |
|---|---|
| `status`, `reason` | `ok`, or `blocked` with why nothing was measured |
| `business`, `businessName`, `placeId`, `cid` | the business as you gave it, its name on Google Maps and its ids |
| `keyword` | the keyword searched |
| `searchedDepth`, `zoom`, `scrapedAt` | how deep it looked (20 or 40), the map zoom, and when |
| `location` | the location as you wrote it |
| `locationName`, `locationAddress`, `lat`, `lng` | where Google Maps placed it, and where the search was made |
| `locationCategory` | `null` for a city, an area or coordinates; `Postal code` for a ZIP; a category (such as `Museum`) when a name led to a single place — write the state or country to be precise |
| `rank` | the business's position among Google Maps' places there; `null` when it is not within `searchedDepth` — never a made-up number |
| `inLocalPack`, `inTop20` | rank 1–3, rank 1–20 |
| `leaderName` | who is #1 in that location |
| `localPack` | the top three there, with place id, CID, rating, review count and category |
| `resultsSeen` | how many places the search listed (fewer than 20 where Google Maps lists fewer) |
| `mapsUrl` | the same search on Google Maps, to look at it yourself |

On a `blocked` check nothing was measured, so `rank`, `inLocalPack` and `inTop20` are all `null` and it is not charged.

#### A summary (free)

| field | meaning |
|---|---|
| `business`, `businessName`, `businessAddress`, `placeId`, `cid`, `keyword` | which business and keyword it sums up |
| `plannedLocations`, `blockedLocations` | locations placed on the map, and those where Google refused or did not answer (free) |
| `reason` | what was left out, if anything: locations past your spending limit or blocked |
| `checkedLocations`, `foundLocations` | locations searched, and those where the business appears |
| `localPackLocations`, `firstPlaceLocations` | locations where it is in the 3-pack, and where it is #1 |
| `averageRank` | average rank over the locations where it appears |
| `averageRankWithMisses` | average rank counting a location where it does not appear as one past the depth searched (21 or 41) |
| `shareOfLocalVoice` | % of locations where the business is in the 3-pack |
| `bestLocation`, `localPackIn`, `notFoundIn` | its best location and rank, the locations where it is in the pack, and the ones where it is invisible |
| `competitors` | up to 10 places, ranked by how often they are #1 and in the 3-pack across your locations, with their average rank |

### Pricing

Pay per event, no start fee:

| event | price |
|---|---|
| `location-check` — one business's rank for one keyword in one location (with the 3-pack and the leader) | **US$0.003** |
| `extra-depth-check` — ranks 21–40 for that check, only when you ask for it and only where the business is not in the first twenty | US$0.002 |

One business, 3 keywords and 15 towns is 45 checks = **US$0.135**. Summaries, unresolved businesses and locations, and
blocked checks are free. The spending limit of your run is respected **before** any search is sent: the Actor plans only
the checks your limit pays for (counting the second page where you asked for it) and says in the summary which
locations it left out.

### Stops when Google asks for verification

The Actor reads the same answers Google Maps gives its own web page, at a moderate pace. **If Google ever asks for
verification** — its "unusual traffic" page, a CAPTCHA, a JavaScript check, or any answer that is not Google Maps'
normal data — **the run stops sending requests** and every check left is a free `blocked` row that says so. It never
solves, bypasses or replays a challenge, and it does not hide where it searches from: the location travels in the map
view, as it does when you move the map yourself. Plain overloads (HTTP 429 or 5xx without a challenge) are retried
through Apify's datacenter proxy; checks that still get no answer are free.

### Limits

- Up to 10 businesses, 20 keywords and 100 locations per run.
- A location name is placed where Google Maps' first result for it lies: `Springfield` alone lands on one Springfield's
  landmark, `Springfield, IL` on the city. `locationName` and `locationCategory` show what it resolved to.
- Ranks are those of Google Maps' place list for the search in each location (the "Places" list a customer scrolls).
  Paid ads are not counted.
- Results depend on the zoom and on Google's personalisation; this Actor sees them as a signed-out visitor.

### Is it legal?

It reads publicly visible search results, at a pace a person browsing the map could produce, without logging in and
without collecting personal data: the places are businesses as they present themselves on Google Maps. Use the data in
line with Google's terms and the laws that apply to you. This Actor is not affiliated with, endorsed by or sponsored by
Google.

# Actor input Schema

## `businesses` (type: `array`):

The business to check, one per line: its <b>Google Maps link</b> (the place page, or a <code>maps.app.goo.gl</code> share link), its <b>place id</b> (<code>ChIJ…</code>) or its <b>CID</b>. It is matched by these ids, never by its name, so a branch with the same name is never mistaken for yours. Add competitors to rank them in the same searches. Up to 10.

## `keywords` (type: `array`):

What customers type in Google Maps, one per line (<code>dentist</code>, <code>emergency dentist</code>). Each keyword is searched once in every location. Up to 20.

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

Where your customers search from, one per line: a <b>city</b> (<code>Austin, TX</code>), a <b>ZIP or postal code</b> (<code>78704</code>, <code>LS1 4AP</code>) or <b>coordinates</b> (<code>30.2672, -97.7431</code>). A name is placed where Google Maps' first result for it lies; add the state or country when it is ambiguous (<code>Springfield, IL</code>). Each location is one search per keyword. Up to 100.

## `checkTop40` (type: `boolean`):

Google Maps lists twenty places a page. Off: where the business is not in the first twenty, its rank is empty. On: a second page is read in those locations (charged as extra depth) to find ranks 21–40.

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

The Google Maps zoom each location is searched at, as a phone would show it: 13 is part of a city (the usual setting of local rank tools), 15 a neighbourhood, 17 a few streets.

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

Language of names and categories, as a code: <code>en</code>, <code>es</code>, <code>de</code>, <code>pt-BR</code>.

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

Two-letter country code Google answers for (<code>us</code>, <code>gb</code>, <code>ca</code>, <code>au</code>). It also decides how a ZIP or place name is read: <code>LS1 4AP</code> wants <code>gb</code>.

## Actor input object example

```json
{
  "businesses": [
    "https://www.google.com/maps/place/Swish+Dental+Downtown/data=!4m2!3m1!1s0x8644b5b434046875:0x644e6eb422388b7a"
  ],
  "keywords": [
    "dentist"
  ],
  "locations": [
    "Austin, TX",
    "78704",
    "Round Rock, TX"
  ],
  "checkTop40": false,
  "zoom": 13,
  "language": "en",
  "country": "us"
}
```

# Actor output Schema

## `rows` (type: `string`):

No description

## `summary` (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 = {
    "businesses": [
        "https://www.google.com/maps/place/Swish+Dental+Downtown/data=!4m2!3m1!1s0x8644b5b434046875:0x644e6eb422388b7a"
    ],
    "keywords": [
        "dentist"
    ],
    "locations": [
        "Austin, TX",
        "78704",
        "Round Rock, TX"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("steadyscrape/local-pack-rank-checker").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 = {
    "businesses": ["https://www.google.com/maps/place/Swish+Dental+Downtown/data=!4m2!3m1!1s0x8644b5b434046875:0x644e6eb422388b7a"],
    "keywords": ["dentist"],
    "locations": [
        "Austin, TX",
        "78704",
        "Round Rock, TX",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("steadyscrape/local-pack-rank-checker").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 '{
  "businesses": [
    "https://www.google.com/maps/place/Swish+Dental+Downtown/data=!4m2!3m1!1s0x8644b5b434046875:0x644e6eb422388b7a"
  ],
  "keywords": [
    "dentist"
  ],
  "locations": [
    "Austin, TX",
    "78704",
    "Round Rock, TX"
  ]
}' |
apify call steadyscrape/local-pack-rank-checker --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,steadyscrape/local-pack-rank-checker"
        }
    }
}
```

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/kSnjCBPg4q11pYALg/builds/uUetz0heiZwNXQ5ta/openapi.json
