# Google Maps Rank Tracker — Local SEO Positions & Reviews (`northbell/google-maps-rank-tracker`) Actor

Track where your business ranks on Google Maps for every keyword and city, daily, with rank changes and review growth - not just a list of places. Give keywords, locations and the businesses to watch; get position, change since the last run and review count. $5 per 1,000 results. No login.

- **URL**: https://apify.com/northbell/google-maps-rank-tracker.md
- **Developed by:** [Northbell](https://apify.com/northbell) (community)
- **Categories:** Marketing, SEO tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $4.00 / 1,000 tracked results

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/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 Rank Tracker — Local SEO Positions & Reviews

Give it **keywords, locations and the businesses to track** (a name, a website, a Google Maps link or a place ID) and get, for every keyword in every area, the **position at which each business appears on Google Maps**, the places directly above it, **who holds the top spots** (by name, with their moves since the last run), and its rating and review count. **$5 per 1,000 tracked results**, no login, no API key, no browser.

Run it every day on a schedule and each row also says **what changed since the last run**: how many positions the business moved (positive = moved up), how many reviews it gained, whether its rating changed, and how many reviews the top ten places gained over the same time — so you see whether your reviews grow faster than the places around you. Google Maps only shows you today's list; this keeps the history for you. It does not output the list of places it reads - only the position and review numbers of the businesses you watch, and the names and positions of the top few places (no addresses, phones, hours or reviews of other businesses).

### What you get

One row per keyword × location × tracked business (`type: "watch-summary"`):

| Field | What it is |
|---|---|
| `search`, `keyword`, `location` | the search that was run (`"dentist in Austin, TX"`) and its two parts (empty `keyword` and `location` for a free-text search) |
| `watched` | the business as you gave it |
| `found` | `true` if it is among the places read for this search |
| `rank` | its best position in Google's list for that search (1 = first); empty when not found |
| `previousRank`, `rankChange`, `trend` | position in your last run, positions moved (**positive = moved up**), and `up`, `down`, `same`, `entered` (was not in the last list), `dropped-out` (was in it, is not now), `not-found` or `no-history` (first run, or the last run read too few places to say) |
| `matchedBy`, `matchedPlaces` | how it was matched (`placeId`, `website` or `name`) and how many places matched; above 1 means a chain or a shared website, and `rank` is the best of them |
| `placeId`, `name`, `address` | Google's place ID, the listing's name and address, so you can check it is the right business |
| `rating`, `ratingChange` | Google rating and its change since the last run |
| `reviewCount`, `reviewCountChange` | number of reviews and the reviews gained since the last run |
| `competitorsAbove` | up to 5 businesses ranked directly above it, top first (empty list at position 1; people-named listings and closed places are left out) |
| `topResults` | the top places for the search, 5 by default (*Top places to show*, up to 20): `rank`, `name`, `previousRank`, `rankChange` (positive = moved up), `newInTop` (`true` = was not in the top last run) and `watched` (`true` = one of yours). Shown also when your business is not found. People-named listings and closed places are skipped, so ranks can jump (1, 3, 4 …) |
| `leftTopResults` | places that were in the top last run and are not now: `name`, `previousRank` and `rank` today (empty when it is below the places read). Empty on the first run |
| `topTenAverageRating`, `topTenAverageReviewCountChange` | the average rating of the top ten places other than yours, and the average review growth of those that were in the last list too — your growth against the places around you |
| `placesRead` | how many places were read for the search |
| `note` | why a business was not found, or that several places matched |
| `observedAt` | when it was read |

The change fields are empty on the first run. A real row (read 2026-10-09, the fourth of four runs a few minutes apart, tracking a website; `topResults` cut to two of the five here):

```json
{ "type": "watch-summary", "search": "emergency dentist in Austin, TX", "keyword": "emergency dentist", "location": "Austin, TX",
  "watched": "treatyoakdental.com", "found": true, "rank": 38, "previousRank": 35, "rankChange": -3, "trend": "down",
  "matchedBy": "website", "matchedPlaces": 1, "placeId": "ChIJRRBY-XS1RIYRIsb3UXu9kfw", "name": "Treaty Oak Dental",
  "address": "2905 San Gabriel St #202, Austin, TX 78705", "rating": 4.8, "ratingChange": 0, "reviewCount": 171, "reviewCountChange": 0,
  "competitorsAbove": ["River Park Cosmetic & Implant Dentistry", "Lucent Dentistry", "myDental at Norwood Park", "Belterra Dental", "Daybreak Dental"],
  "topResults": [
    { "rank": 1, "name": "Emergency Dentist of Austin", "previousRank": 1, "rankChange": 0, "newInTop": false, "watched": false },
    { "rank": 2, "name": "Austin Emergency Dental", "previousRank": 2, "rankChange": 0, "newInTop": false, "watched": false } ],
  "leftTopResults": [],
  "topTenAverageRating": 4.76, "topTenAverageReviewCountChange": 0, "placesRead": 40, "note": null }
```

In the run before it, `Urgent Dental Response` had come into the top five from position 7 (`"previousRank": 7, "rankChange": 2, "newInTop": true`), and positions 3 and 4 had swapped places (`rankChange` 1 and -1).

Per search there is also one free row (`type: "summary"`): `placesRead`, `averageRating`, `newSinceLastRun`, `leftSinceLastRun`, `watchedFound`, `reachedEnd` (`true` when Google ran out of places before your limit), `reviewCountsMissing`, and a `note` when a search returned nothing. If a run is stopped before its time limit, one free `notice` row lists the searches that were not read.

### Run it again and again

- **On a schedule** — save your input as a task and add a **Schedule**: the same keywords and areas every day. Send each run to Google Sheets, Slack or email with an integration on the task.
- **Watch for changes** — Nothing to switch on: from the second run each row says how many positions the business moved, how many reviews it gained and how the top places changed since the last run.
- **In bulk** — Put every keyword in **Keywords**, every area in **Locations** and every business in **Businesses to track**: one run checks all the combinations.

### Use it for

- **Local businesses** — where do I show up for "dentist" in my town and the next town over, and did last week's review push move me?
- **SEO and marketing agencies** — one daily run for all clients: each client's name or website as a tracked business, every service keyword, every service area
- **Multi-location businesses** — track each location by its Google Maps link or place ID and compare areas
- **Competitor watch** — track the businesses that rank above you and see their review growth next to yours

### Input

```json
{
  "keywords": ["dentist", "emergency dentist"],
  "locations": ["Austin, TX", "Round Rock, TX"],
  "watch": ["treatyoakdental.com", "Austin Lifetime Dental", "ChIJ9eZOciDMRIYRSVHxKyz66So"],
  "maxPlacesPerSearch": 60
}
```

- **Keywords × Locations** — every keyword is searched in every location as `keyword in location`: the example above is 4 searches. You can also give **free-text searches** (`"dentist in Manchester, UK"`) next to or instead of them. Up to 100 searches per run.
- **Businesses to track** — required, up to 25 per run, each matched against every result. A **website** (`example.com`; a subdomain matches too, and `example.com/austin` only matches pages under that path) is the sturdiest for a business with one site. A **place ID** (`ChIJ…` or `0x…:0x…`) or a **Google Maps place link** follows exactly one location. A **name** is compared ignoring case, accents and punctuation; `Treaty Oak Dental` also matches `Treaty Oak Dental – Downtown` (a branch after a dash, comma or bracket) and `matchedPlaces` shows when that gives several. Short `maps.app.goo.gl` links cannot be followed; open one and copy the full address.
- **How deep to look** — 1 to 200, default 60. A business below this position is `found: false`.
- **Top places to show** — 0 to 20, default 5. How many of the top places to name in `topResults`; 0 turns it off.
- **Language** — a language code (`en`, `de`, `ja`, `pt-BR` …).

History is kept per search text and per language in a named key-value store in your Apify account, so the second run already shows changes. Schedule it (Apify → Schedules) for a daily log. Keep the wording of a search the same from run to run; a new wording is a new search with no history. A business you add later gets its previous position from the last list of places if it was in it.

### Pricing

**$5 per 1,000 tracked results** (one per keyword × location × business), plus $0.01 per run. The summary row, error rows and a search that returned no list are free; nothing is charged for a tracked business of a search that could not be read. Reading deeper costs nothing extra. Example: 2 keywords × 2 cities × 3 businesses is 12 results = $0.06 + $0.01 per daily run.

### Notes

- **The location is the one in the search.** Google ranks local results by the place named in the query, and this Actor sends no location of its own. Checked on 2026-10-09 from a PC in Japan (not from Apify's servers, so a US server address was not compared): `keywords: ["dentist"]` with `locations: ["Austin, TX"]` sends exactly the text `dentist in Austin, TX`, the same request as typing that search, and `plumber in Aurora, CO` returned the same 40 places in the same order whether the Google session was opened with that search, with a search in Tokyo or with no cookie at all.
- **Ranks are for a US visitor.** The Actor asks Google as a visitor from the US. Asking `plumber in Denver, CO` as a visitor from the UK, Japan, Germany or Australia on 2026-10-09 gave 9, 8, 8 and 9 of the same top ten places as the US request, but in the same position for only 4, 2, 4 and 2 of them (top 40: 34 of the same places each, 2 to 5 in the same position; a second US request was identical). What a searcher sees at home can differ from your rank here; there is no country setting.
- **Wording matters.** `plumber Denver CO` returned 38 of the 40 places of `plumber in Denver, CO`, only 9 of them in the same position. Pick one wording per keyword and keep it.
- **Give a place.** A search without one (`plumber`) returned places from wherever Google guessed: on 2026-10-09 the top 20 were spread over California, Oregon, Washington, Utah, Oklahoma and Maryland. `keywords` therefore always need `locations`.
- **The order wobbles, by keyword.** Google does not always answer the same request with the same list. On 2026-10-09, four reads each of `dentist in Austin, TX` and `plumber in Denver, CO` gave the same 60 places, 58 to 60 of them in the same position. `water heater repair in Aurora, CO` was stable over 10 identical requests in a row in one series, but in a second series 3 of the 10 answers came back in a different order, and two Actor runs a few minutes apart shared only 20 of its 60 places. `emergency dentist in Austin, TX` was the least stable: 6 to 9 of the same top 10 but only 2 in the same position, and 41 to 57 of the same top 60 with 2 or 3 in the same position. A move of one or two positions, or a single day on a volatile keyword, is noise: judge the trend over a week.
- **How deep a search goes is Google's call, and it differs by search.** On 2026-10-09 Google ended the list by itself at 152 to 192 places for coffee in Austin, dentists in Manchester, ramen in Osaka, pharmacies in Berlin and plumbers in Toronto (`reachedEnd: true`); hotels in Lisbon went on past 200. This Actor reads at most 200 per search.
- **Speed.** Four searches of 60 places (240 places, 16 tracked results) took 29 and 33 seconds in two runs on 2026-10-09, about 8 seconds a search. Six searches of up to 200 places (1,048 places) took 143 seconds. If a run is about to hit its time limit, the Actor stops 90 seconds before it, keeps what it has, and lists the searches it did not read in a free `notice` row; run those again or raise the run timeout.
- **Language changes more than labels.** It sets the language of categories, weekday names and hours, and it also shifts which places rank where: for `plumber in Denver, CO` on 2026-10-09, German, Japanese and Spanish each gave 19 of the same top 20 places as English, but only 8, 6 and 6 in the same position (two English reads were identical). Keep the language the same from run to run.
- **Not found.** `found: false` means the business is not among the places read, not that it is not on Google Maps. Raise *How deep to look* (reading deeper costs nothing extra). If a search stops halfway because Google stops answering, a business that was not found so far gets no row at all (an error row says what happened); one that was found still does.
- **Chains and shared websites.** A website or name that matches several places gives one row with the best position and `matchedPlaces` above 1. If the best location is a different place than in the last run, `rankChange` still compares the best positions but `ratingChange` and `reviewCountChange` stay empty (they would mix two places). Use a place ID or a Google Maps place link to follow one location.
- **Needs a list, not one place.** A search for one named place (`Eiffel Tower Paris`) returned no list of places on 2026-10-09; the summary row says so in `note` and no tracking rows are output. Use a category plus an area.
- **Review counts.** Google sometimes leaves the review count out of an answer. The Actor asks again up to three times; `reviewCountsMissing` in the summary row says how many stayed empty (0 in all six searches above).
- **Public listing data only.** Only the numbers and names of the businesses you watch, the names in `competitorsAbove` and the names and positions in `topResults` and `leftTopResults` are output. No reviewer names, review texts, owner names or photos are read. A listing that is clearly one person — a name with a title or credential (`Dr. Jane Doe, DDS`, `Dr. med. dent. Jane Doe`, `Rechtsanwalt Mag. Jane Doe`, `Docteur Jane Doe`) or with an occupation (`Jane Doe - Realtor`) — is left out of `competitorsAbove` and `topResults`. An address line that is an owner's name with a legal form (`Jane Doe e.Kfr`, `Inh. Jane Doe`, `c/o …`, seen on pharmacies in Berlin) is removed from `address`. These are rules of thumb, not guarantees.
- **If Google refuses.** Google can answer HTTP 429 or show a consent or "unusual traffic" page. The Actor waits and asks again, never tries to get around such a page, and reports the search in a free error row. The run fails only if nothing at all could be read. The figures above were measured over plain requests from one ordinary internet connection, and shared server addresses can be limited sooner. Not affiliated with Google.

To track **where pages rank on Bing**, see [Bing Search Rank Tracker](https://apify.com/northbell/bing-search-rank-tracker). For the same kind of ranking on video, see [YouTube Search Rank Tracker](https://apify.com/northbell/youtube-search-rank-tracker). For places from OpenStreetMap, see [OpenStreetMap Places Scraper](https://apify.com/northbell/openstreetmap-places-scraper).

### For AI agents

`{"keywords": ["dentist"], "locations": ["Austin, TX"], "watch": ["treatyoakdental.com"]}` returns one `watch-summary` row per keyword, location and watched business (`rank`, `rankChange`, `trend`, `reviewCount`, `reviewCountChange`, `competitorsAbove`, `topResults` with the top 5 names and their moves, `leftTopResults`) plus one free `summary` row per search. It does not return the places it reads beyond those names and positions. `watch` is required. Row `type` is `watch-summary`, `summary`, `notice` or `error`. Run it on a schedule: change fields are empty on the first run. Always give locations; keep the search wording the same between runs.

# Actor input Schema

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

What people search for, one per line: "dentist", "emergency dentist", "ramen". Every keyword is searched in every location below as "keyword in location".

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

Cities or areas, one per line: "Austin, TX", "Round Rock, TX", "Manchester, UK". Google ranks by the place named in the search, so write the location the way you would type it into Google Maps.

## `watch` (type: `array`):

Required. The businesses whose position you want, one per line (up to 25): a website address (best for a business with one site), the business name, a Google Maps place link (best for one location of a chain), or a place ID (ChIJ... or 0x...:0x...). Each one is matched against every result and gets one row per keyword and location. The Actor outputs only these businesses, not the list of places it reads, so the run fails without at least one.

## `searches` (type: `array`):

Extra searches written exactly as you would type them into Google Maps, with the area in them: "dentist in Manchester, UK". Use this instead of, or next to, keywords and locations. Changes since the last run are kept per search text, so keep the wording the same from run to run.

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

How many places to read for each search, in the order Google lists them (1 to 200). A business below this position is reported as not found. Reading deeper costs nothing extra: you pay per tracked result, not per place read.

## `topResults` (type: `integer`):

How many of the top places to name on each row, with their position and how they moved since the last run (0 to 20; 0 turns it off). Names and positions only - no addresses, phones or hours. Shown also when your business is not found, so you see who holds the top spots.

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

Language code for categories, weekday names and opening hours, e.g. en, de, ja, fr, es, pt-BR, zh-CN. It can also change which places rank where, so keep the same language from run to run: changes since the last run are compared per language.

## Actor input object example

```json
{
  "keywords": [
    "dentist"
  ],
  "locations": [
    "Austin, TX"
  ],
  "watch": [
    "treatyoakdental.com"
  ],
  "maxPlacesPerSearch": 60,
  "topResults": 5,
  "language": "en"
}
```

# Actor output Schema

## `watch` (type: `string`):

Position of each tracked business for every keyword and location, with the change since the last run and review growth.

# 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 = {
    "keywords": [
        "dentist"
    ],
    "locations": [
        "Austin, TX"
    ],
    "watch": [
        "treatyoakdental.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("northbell/google-maps-rank-tracker").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 = {
    "keywords": ["dentist"],
    "locations": ["Austin, TX"],
    "watch": ["treatyoakdental.com"],
}

# Run the Actor and wait for it to finish
run = client.actor("northbell/google-maps-rank-tracker").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 '{
  "keywords": [
    "dentist"
  ],
  "locations": [
    "Austin, TX"
  ],
  "watch": [
    "treatyoakdental.com"
  ]
}' |
apify call northbell/google-maps-rank-tracker --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,northbell/google-maps-rank-tracker"
        }
    }
}
```

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/uPhq1VQalLHSDBB6o/builds/weKfGUeF0kN1PSgsp/openapi.json
