# Google Rank Tracking for SERP & Maps (`maximedupre/google-rank-tracking`) Actor

Track a submitted website domain in Google organic results for each keyword, or check a business across a Google Maps grid. Get positions, result details, local competitors, visibility summaries, and coverage status for a chosen country and language.

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

## Pricing

from $7.20 / 1,000 organic positions

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 rank tracking for organic and local search

SEO teams, agencies, and site owners can check a website's Google organic position or a business's Google Maps visibility for a list of keywords. The Actor returns positions, search result details, coverage status, visibility summaries, local competitors, market findings, and repeat-check history when available. It reads public Google Search and Google Maps results, so it does not read private Google Search Console data.

- Use [**Website Ranking Checker**](https://apify.com/maximedupre/google-rank-tracking/examples/website-ranking-checker) to see where a site appears for your target keywords.
- Use [**Keyword Rank Checker**](https://apify.com/maximedupre/google-rank-tracking/examples/keyword-rank-checker) to check several keywords for one domain in a chosen Google context.
- Use [**Google Keyword Rank Checker**](https://apify.com/maximedupre/google-rank-tracking/examples/google-keyword-rank-checker) to review a domain position and the organic pages around it.
- Use [**Google SEO Ranking Checker**](https://apify.com/maximedupre/google-rank-tracking/examples/google-seo-ranking-checker) to compare organic visibility measures and movement alerts for a keyword list.
- Use [**Check Website Ranking on Google**](https://apify.com/maximedupre/google-rank-tracking/examples/check-website-ranking-on-google) to check a domain in a selected country and language.

#### 📊 Google positions, result evidence, and local visibility

Each dataset row has a result type, Google country, and Google language. Organic rows include the tracked domain position, organic Google results, history, movement alerts, related queries, People Also Ask questions, answer features, paid results, and a visibility summary. Local rows include business details, a scan grid, positions at grid points, competitors, coverage, market density, opportunity areas, review synthesis, a market report, history, and change alerts. A null position means the tracked domain or business was not found within the checked depth or at that grid point. Access status and coverage show when a check is partial.

#### ▶️ Run a Google ranking check

1. Choose Organic rank tracking or Local rank tracking.
2. Add your keywords, Google country, and Google language. For an organic check, add the website domain, depth, and movement threshold. For a local check, add the business, scan center, radius, grid resolution, and priority lens.
3. Run the Actor and open the dataset. Each row keeps the country and language used for its Google check.

Repeat a check with the same settings when you want available history or movement alerts. Google can return partial coverage, so read the status fields before using a position in a report.

#### ⚙️ Input

Choose one result family. Complete the fields for that choice; fields in the other choice are ignored. Keywords, country, and language apply to both choices.

**Input fields**

| Field | Type | What it does |
| --- | --- | --- |
| resultType | string | Choose organic to check a website in Google Search or local to check a business across a Google Maps grid. |
| websiteDomain | string | Domain to track without https:// or a path. Required for Organic rank tracking and ignored for local checks. |
| organicDepth | integer | Number of organic result positions to check for each keyword. A not-found position means the domain was not seen within this depth. Default: 100. |
| organicMovementThreshold | integer | Number of positions a repeat organic check must move to create an alert. Use 1 to flag any position change. Default: 3. |
| localBusinessName | string | Public business name to find in Google Maps. Required for Local rank tracking. |
| localBusinessAddress | string | Address that helps match the right Google Maps listing. Leave it blank when the name is enough. |
| localScanCenter | object | Center of the local grid, with latitude and longitude in decimal degrees. |
| localScanCenter.latitude | number | Latitude from -90 to 90. |
| localScanCenter.longitude | number | Longitude from -180 to 180. |
| localScanRadiusKm | number | Distance from the scan center covered by the local grid, in kilometers. Default: 5. |
| localGridResolution | string | Number of points on each grid side: 3x3, 5x5, or 7x7. Default: 5x5. |
| localPrioritization | string | Lens used to put local market findings first: opportunity, visibility, or competition. Default: opportunity. |
| keywords | array of strings | One or more search terms. Organic checks the domain for each keyword. Local checks the business at each grid point for each keyword. |
| countryCode | string | Uppercase two-letter ISO 3166-1 country code, such as US, used for every Google check. Default: US. |
| languageCode | string | Language code such as en or fr. Use a language-region code such as en-US when needed. Default: en. |

**Example input**

The following payload is copied from the successful hosted default-input run. For an organic check of your own site, also provide websiteDomain.

```json
{
  "resultType": "organic",
  "organicDepth": 100,
  "organicMovementThreshold": 3,
  "localScanRadiusKm": 5,
  "localGridResolution": "5x5",
  "localPrioritization": "opportunity",
  "keywords": [
    "seo tools"
  ],
  "countryCode": "US",
  "languageCode": "en"
}
```

#### 🧾 Output

Each dataset item is one of two mutually exclusive shapes. The tables below include the shared fields and every nested field in each shape. Types such as integer or null show when the schema uses null to mean that Google did not return a position or value.

**Organic output shape**

| Field | Type | What it does |
| --- | --- | --- |
| resultType | string | organic for this row. |
| countryCode | string | Two-letter Google country context. |
| languageCode | string | Google language context. |
| organic | object | Organic tracking branch. |
| organic.websiteDomain | string | Domain checked in Google Search. |
| organic.checkedDepth | integer | Number of organic positions checked for each keyword. |
| organic.movementThreshold | integer | Position change needed to create a movement alert. |
| organic.coverage | object | Coverage of the submitted organic keyword checks. |
| organic.coverage.status | string | Whether the requested organic checks reached the requested depth: complete or partial. |
| organic.coverage.requestedKeywords | integer | Number of keywords submitted for the run. |
| organic.coverage.completedKeywords | integer | Number of submitted keywords with a successful result. |
| organic.keywordResults | array of objects | Organic result and available Google evidence for each keyword. |
| organic.keywordResults\[].keyword | string | Keyword checked in Google Search. |
| organic.keywordResults\[].position | integer or null | Tracked domain position. Null means the domain was not seen within the checked depth. |
| organic.keywordResults\[].accessStatus | string | Whether Google returned the requested depth for the keyword: complete or partial. |
| organic.keywordResults\[].organicResults | array of objects | Organic Google results seen for the keyword. |
| organic.keywordResults\[].organicResults\[].position | integer | Organic result position. |
| organic.keywordResults\[].organicResults\[].title | string | Result title shown by Google. |
| organic.keywordResults\[].organicResults\[].url | string | Result page URL. |
| organic.keywordResults\[].organicResults\[].domain | string | Result domain used to compare competing domains. |
| organic.keywordResults\[].organicResults\[].displayedUrl | string, when available | URL text shown by Google. |
| organic.keywordResults\[].organicResults\[].snippet | string, when available | Result snippet shown by Google. |
| organic.keywordResults\[].organicResults\[].sourceName | string, when available | Source name shown by Google. |
| organic.keywordResults\[].history | array of objects | Prior checks for the keyword when history is available. |
| organic.keywordResults\[].history\[].checkedAt | string | Time of the prior check. |
| organic.keywordResults\[].history\[].position | integer or null | Prior position. Null means the domain was not found within that check depth. |
| organic.keywordResults\[].movementAlerts | array of objects | Material position changes for the keyword. |
| organic.keywordResults\[].movementAlerts\[].kind | string | Movement type: gain, loss, entry, or dropout. |
| organic.keywordResults\[].movementAlerts\[].previousPosition | integer or null | Earlier position, or null when the domain was not found then. |
| organic.keywordResults\[].movementAlerts\[].currentPosition | integer or null | Current position, or null when the domain is not found within the current depth. |
| organic.keywordResults\[].movementAlerts\[].threshold | integer | Threshold that triggered the alert. |
| organic.keywordResults\[].movementAlerts\[].checkedAt | string | Time of the current check. |
| organic.keywordResults\[].relatedQueries | array of strings | Related Google query suggestions when available. |
| organic.keywordResults\[].peopleAlsoAsk | array of objects | People Also Ask questions shown by Google. |
| organic.keywordResults\[].peopleAlsoAsk\[].question | string | People Also Ask question. |
| organic.keywordResults\[].featuredAnswer | object or null | Featured answer content when Google shows it. |
| organic.keywordResults\[].featuredAnswer.content | string | Featured answer content. |
| organic.keywordResults\[].featuredAnswer.sourceName | string, when available | Featured answer source name. |
| organic.keywordResults\[].featuredAnswer.sourceUrl | string, when available | Featured answer source URL. |
| organic.keywordResults\[].aiOverview | object or null | AI Overview content when Google shows it. |
| organic.keywordResults\[].aiOverview.content | string | AI Overview content. |
| organic.keywordResults\[].aiOverview.sources | array of objects | Sources cited by the AI Overview. |
| organic.keywordResults\[].aiOverview.sources\[].title | string | Cited source title. |
| organic.keywordResults\[].aiOverview.sources\[].url | string | Cited source URL. |
| organic.keywordResults\[].paidResults | array of objects | Paid search ads and shopping results when Google provides them. |
| organic.keywordResults\[].paidResults\[].kind | string | Paid result kind: searchAd or shopping. |
| organic.keywordResults\[].paidResults\[].position | integer, when available | Paid result position. |
| organic.keywordResults\[].paidResults\[].title | string, when available | Paid result title. |
| organic.keywordResults\[].paidResults\[].url | string, when available | Paid result URL. |
| organic.keywordResults\[].paidResults\[].displayedUrl | string, when available | Paid result URL text. |
| organic.keywordResults\[].paidResults\[].snippet | string, when available | Paid result snippet. |
| organic.keywordResults\[].paidResults\[].sourceName | string, when available | Paid result source name. |
| organic.keywordResults\[].paidResults\[].price | string, when available | Displayed shopping price. |
| organic.visibilitySummary | object | Aggregate organic visibility across the submitted keywords. |
| organic.visibilitySummary.averagePosition | number or null | Average tracked domain position across found keywords. |
| organic.visibilitySummary.bestPosition | integer or null | Best tracked domain position across found keywords. |
| organic.visibilitySummary.top3Count | integer | Number of submitted keywords where the domain reached the top three. |
| organic.visibilitySummary.top10Count | integer | Number of submitted keywords where the domain reached the top ten. |

**Organic example row**

This is a complete row from a successful current-beta organic run.

```json
{
  "resultType": "organic",
  "countryCode": "US",
  "languageCode": "en",
  "organic": {
    "websiteDomain": "example.com",
    "checkedDepth": 100,
    "movementThreshold": 3,
    "coverage": {
      "status": "complete",
      "requestedKeywords": 1,
      "completedKeywords": 1
    },
    "keywordResults": [
      {
        "keyword": "example.com",
        "position": 1,
        "accessStatus": "partial",
        "organicResults": [
          {
            "position": 1,
            "title": "What is @example.com?",
            "url": "https://www.example.com/",
            "domain": "www.example.com",
            "displayedUrl": "30+ answers · 9 years ago",
            "snippet": "What is @example.com?",
            "sourceName": "Quora"
          },
          {
            "position": 2,
            "title": "example.com",
            "url": "https://en.wikipedia.org/wiki/Example.com",
            "domain": "en.wikipedia.org",
            "displayedUrl": "https://en.wikipedia.org › wiki › Example",
            "snippet": "The domain names example.com, example.net, example.org, and example.edu are second-level domain names in the Domain Name System of the Internet.Read more",
            "sourceName": "Wikipedia"
          },
          {
            "position": 3,
            "title": "WWW.example.com or HTTP://example.com – which one is ...",
            "url": "https://webmasters.stackexchange.com/",
            "domain": "webmasters.stackexchange.com",
            "displayedUrl": "https://webmasters.stackexchange.com › questions › ww...",
            "snippet": "Nov 30, 2010 — http://example.com www.example.com http://www.example.com example.com Which of these would you choose as your favourite to work with from 2016 onward? ...",
            "sourceName": "Webmasters Stack Exchange"
          },
          {
            "position": 4,
            "title": "Example Domains",
            "url": "https://www.iana.org/help/example-domains",
            "domain": "www.iana.org",
            "displayedUrl": "https://www.iana.org › help › example-domains",
            "snippet": "4 days ago — A number of domains such as example.com and example.org are maintained for documentation purposes. These domains may be used as illustrative ...Read more",
            "sourceName": "Internet Assigned Numbers Authority (IANA)"
          },
          {
            "position": 5,
            "title": "Example.com - valid and useful site",
            "url": "https://packetpushers.net/",
            "domain": "packetpushers.net",
            "displayedUrl": "https://packetpushers.net › blog › example-com-valid-a...",
            "snippet": "Nov 25, 2019 — These domains may be used as illustrative examples in documents without prior coordination with us. They are not available for registration or ...",
            "sourceName": "Packet Pushers"
          },
          {
            "position": 6,
            "title": "What's the example.com domain?",
            "url": "https://www.plothost.com/",
            "domain": "www.plothost.com",
            "displayedUrl": "https://www.plothost.com › KB › Internet",
            "snippet": "Aug 24, 2017 — Example.com (example.org, example.net) are domains that can be used as examples in documents/papers/websites etc. They were specifically created ...Read more",
            "sourceName": "PlotHost"
          },
          {
            "position": 7,
            "title": "Typo traps: analyzing traffic to exmaple.com (or is it ...",
            "url": "https://blog.cloudflare.com/typo-traps-analyzing-traffic-to-exmaple-com-or-is-it-example-com/",
            "domain": "blog.cloudflare.com",
            "displayedUrl": "https://blog.cloudflare.com › typo-traps-analyzing-traffic...",
            "snippet": "Sep 22, 2023 — Cloudflare has owned exmaple.com for a few years now, but don't confuse it with example.com! example.com is a reserved domain name set by the ...Read more",
            "sourceName": "Cloudflare Blog"
          },
          {
            "position": 8,
            "title": "example.com is a better choice for an example domain",
            "url": "https://github.com/",
            "domain": "github.com",
            "displayedUrl": "https://github.com › mdn › content › issues",
            "snippet": "Jun 9, 2025 — What information was incorrect, unhelpful, or incomplete? foo.com is a real domain, so I think it's better to use example.com when writing ...Read more",
            "sourceName": "GitHub"
          }
        ],
        "history": [
          {
            "checkedAt": "2026-10-01T00:18:39.415Z",
            "position": 1
          }
        ],
        "movementAlerts": [],
        "relatedQueries": [],
        "peopleAlsoAsk": [
          {
            "question": "What is example.com used for?"
          },
          {
            "question": "Is example.com a real email address?"
          },
          {
            "question": "Who owns the example.com domain name?"
          },
          {
            "question": "What does someone example.com mean?"
          }
        ],
        "paidResults": []
      }
    ],
    "visibilitySummary": {
      "averagePosition": 1,
      "bestPosition": 1,
      "top3Count": 1,
      "top10Count": 1
    }
  }
}
```

**Local output shape**

| Field | Type | What it does |
| --- | --- | --- |
| resultType | string | local for this row. |
| countryCode | string | Two-letter Google country context. |
| languageCode | string | Google language context. |
| local | object | Google Maps local tracking branch. |
| local.business | object | Public data for the tracked business. |
| local.business.name | string | Public business name. |
| local.business.address | string, when available | Public street address. |
| local.business.website | string, when available | Public business website. |
| local.business.phone | string, when available | Public business phone number. |
| local.business.location | object, when available | Public business location. |
| local.business.location.latitude | number | Business latitude. |
| local.business.location.longitude | number | Business longitude. |
| local.business.rating | number, when available | Google rating from zero to five. |
| local.business.reviewCount | integer, when available | Number of Google reviews. |
| local.grid | object | Local scan center, radius, and resolution. |
| local.grid.center | object | Center of the local scan grid. |
| local.grid.center.latitude | number | Grid center latitude. |
| local.grid.center.longitude | number | Grid center longitude. |
| local.grid.radiusKm | number | Distance from the center covered by the grid. |
| local.grid.resolution | string | Number of grid points on each side. |
| local.priorityLens | string | Lens used to order local market findings: opportunity, visibility, or competition. |
| local.coverage | object | Coverage of submitted local keywords and grid points. |
| local.coverage.status | string | Whether all local checks were completed: complete or partial. |
| local.coverage.requestedKeywords | integer | Number of local keywords submitted. |
| local.coverage.completedKeywords | integer | Number of local keywords with a successful result. |
| local.coverage.requestedPoints | integer | Total grid points requested across the keywords. |
| local.coverage.checkedPoints | integer | Total grid points with a successful local result. |
| local.keywordResults | array of objects | Local grid results for the submitted keywords. |
| local.keywordResults\[].keyword | string | Keyword checked in Google Maps. |
| local.keywordResults\[].coverage | object | Coverage for this keyword's grid checks. |
| local.keywordResults\[].coverage.status | string | Whether all points for this keyword were checked: complete or partial. |
| local.keywordResults\[].coverage.requestedPoints | integer | Grid points requested for the keyword. |
| local.keywordResults\[].coverage.checkedPoints | integer | Grid points with a successful result. |
| local.keywordResults\[].visibilitySummary | object | Average, best, worst, and Share of Local Voice measures. |
| local.keywordResults\[].visibilitySummary.averagePosition | number or null | Average tracked business position across found grid points. |
| local.keywordResults\[].visibilitySummary.bestPosition | integer or null | Best tracked business position across grid points. |
| local.keywordResults\[].visibilitySummary.worstPosition | integer or null | Worst tracked business position across grid points. |
| local.keywordResults\[].visibilitySummary.shareOfLocalVoice | number | Tracked business share of observed local result positions, from zero to one. |
| local.keywordResults\[].gridPoints | array of objects | Successful local results at the requested grid points. |
| local.keywordResults\[].gridPoints\[].location | object | Grid point location. |
| local.keywordResults\[].gridPoints\[].location.latitude | number | Grid point latitude. |
| local.keywordResults\[].gridPoints\[].location.longitude | number | Grid point longitude. |
| local.keywordResults\[].gridPoints\[].position | integer or null | Tracked business position at the point. Null means it was not seen in the local results. |
| local.keywordResults\[].gridPoints\[].accessStatus | string | Whether Google returned the requested local results: complete or partial. |
| local.keywordResults\[].gridPoints\[].competitors | array of objects | Leading competing businesses at the grid point. |
| local.keywordResults\[].gridPoints\[].competitors\[].position | integer | Competitor position at the grid point. |
| local.keywordResults\[].gridPoints\[].competitors\[].business | object | Public data for a grid-point competitor. It uses the Business fields listed below. |
| local.keywordResults\[].gridPoints\[].competitors\[].business.name | string | Competing business name. |
| local.keywordResults\[].gridPoints\[].competitors\[].business.address | string, when available | Competing business address. |
| local.keywordResults\[].gridPoints\[].competitors\[].business.website | string, when available | Competing business website. |
| local.keywordResults\[].gridPoints\[].competitors\[].business.phone | string, when available | Competing business phone. |
| local.keywordResults\[].gridPoints\[].competitors\[].business.location | object, when available | Competing business location. |
| local.keywordResults\[].gridPoints\[].competitors\[].business.location.latitude | number | Competing business latitude. |
| local.keywordResults\[].gridPoints\[].competitors\[].business.location.longitude | number | Competing business longitude. |
| local.keywordResults\[].gridPoints\[].competitors\[].business.rating | number, when available | Competing business Google rating. |
| local.keywordResults\[].gridPoints\[].competitors\[].business.reviewCount | integer, when available | Competing business review count. |
| local.keywordResults\[].competitors | array of objects | Leading competitors across the local grid. |
| local.keywordResults\[].competitors\[].business | object | Public data for a local competitor. |
| local.keywordResults\[].competitors\[].business.name | string | Competitor business name. |
| local.keywordResults\[].competitors\[].business.address | string, when available | Competitor business address. |
| local.keywordResults\[].competitors\[].business.website | string, when available | Competitor business website. |
| local.keywordResults\[].competitors\[].business.phone | string, when available | Competitor business phone. |
| local.keywordResults\[].competitors\[].business.location | object, when available | Competitor business location. |
| local.keywordResults\[].competitors\[].business.location.latitude | number | Competitor business latitude. |
| local.keywordResults\[].competitors\[].business.location.longitude | number | Competitor business longitude. |
| local.keywordResults\[].competitors\[].business.rating | number, when available | Competitor business Google rating. |
| local.keywordResults\[].competitors\[].business.reviewCount | integer, when available | Competitor business review count. |
| local.keywordResults\[].competitors\[].averagePosition | number or null | Competitor average position across observed grid points. |
| local.keywordResults\[].competitors\[].bestPosition | integer | Competitor best observed position. |
| local.keywordResults\[].competitors\[].worstPosition | integer | Competitor worst observed position. |
| local.market | object | Local market density, saturation, opportunities, and review findings. |
| local.market.competitorDensity | number | Average number of observed competing businesses per checked grid point. |
| local.market.saturation | string | Descriptive saturation level: low, medium, or high. |
| local.market.marketState | string | Descriptive market state: underserved, balanced, or competitive. |
| local.market.opportunityAreas | array of objects | Underserved local areas and their assessments. |
| local.market.opportunityAreas\[].location | object | Opportunity area location. |
| local.market.opportunityAreas\[].location.latitude | number | Opportunity area latitude. |
| local.market.opportunityAreas\[].location.longitude | number | Opportunity area longitude. |
| local.market.opportunityAreas\[].assessment | string | Plain language assessment of the local opportunity. |
| local.market.opportunityAreas\[].priority | string | Opportunity priority: high, medium, or low. |
| local.market.reviewSynthesis | object, when available | Aggregate review sentiment and themes. |
| local.market.reviewSynthesis.sentiment | string | Aggregate review sentiment: positive, mixed, or negative. |
| local.market.reviewSynthesis.themes | array of strings | Common themes in the available reviews. |
| local.market.reviewSynthesis.reviewCount | integer, when available | Number of reviews included in the synthesis. |
| local.marketReport | string | Readable summary of the market snapshot, opportunities, and competitor findings. |
| local.history | array of objects | Prior local scans when history is available. |
| local.history\[].checkedAt | string | Time of the prior local scan. |
| local.history\[].keyword | string | Local keyword covered by the prior scan. |
| local.history\[].averagePosition | number or null | Tracked business average position in the prior scan. |
| local.history\[].rating | number or null | Tracked business rating in the prior scan. |
| local.history\[].reviewCount | integer or null | Tracked business review count in the prior scan. |
| local.changeAlerts | array of objects | New competitor, closure, rating, review, and rank changes found by comparing scans. |
| local.changeAlerts\[].keyword | string | Local keyword linked to the change. |
| local.changeAlerts\[].kind | string | Change type: newCompetitor, closure, ratingChange, rankChange, reviewChange, or mover. |
| local.changeAlerts\[].businessName | string | Business linked to the change. |
| local.changeAlerts\[].previousPosition | integer or null | Earlier local position when available. |
| local.changeAlerts\[].currentPosition | integer or null | Current local position when available. |
| local.changeAlerts\[].previousRating | number or null | Earlier Google rating when available. |
| local.changeAlerts\[].currentRating | number or null | Current Google rating when available. |
| local.changeAlerts\[].previousReviewCount | integer or null | Earlier Google review count when available. |
| local.changeAlerts\[].currentReviewCount | integer or null | Current Google review count when available. |

**Local example row**

The row below is shortened from a successful current-beta local run to keep the grid and repeated competitor data readable. All shown values are genuine. Omitted arrays or nested data are marked with the JSON string "...".

```json
{
  "resultType": "local",
  "countryCode": "US",
  "languageCode": "en",
  "local": {
    "business": {
      "name": "Starbucks Coffee Company",
      "address": "Starbucks Coffee Company, 1912 Pike Pl, Seattle, WA 98101",
      "website": "https://www.starbucks.com/store-locator/store/11676/",
      "phone": "+12064488762",
      "location": {
        "latitude": 47.61004,
        "longitude": -122.34259
      },
      "rating": 4.5,
      "reviewCount": 7171
    },
    "grid": {
      "center": {
        "latitude": 47.6097,
        "longitude": -122.3421
      },
      "radiusKm": 1,
      "resolution": "3x3"
    },
    "priorityLens": "opportunity",
    "coverage": {
      "status": "complete",
      "requestedKeywords": 1,
      "completedKeywords": 1,
      "requestedPoints": 9,
      "checkedPoints": 9
    },
    "keywordResults": [
      {
        "keyword": "Starbucks",
        "coverage": {
          "status": "complete",
          "requestedPoints": 9,
          "checkedPoints": 9
        },
        "visibilitySummary": {
          "averagePosition": 2,
          "bestPosition": 2,
          "worstPosition": 2,
          "shareOfLocalVoice": 0.3333333333333333
        },
        "gridPoints": [
          {
            "location": {
              "latitude": 47.60069099099099,
              "longitude": -122.3554629658123
            },
            "position": null,
            "accessStatus": "complete",
            "competitors": []
          },
          "..."
        ],
        "competitors": [
          {
            "business": {
              "name": "Starbucks Coffee Company",
              "address": "1101 Alaskan Wy, Seattle, WA 98101",
              "website": "https://www.starbucks.com/store-locator/store/16417/",
              "location": {
                "latitude": 47.604859999999995,
                "longitude": -122.33971
              },
              "rating": 3.8,
              "reviewCount": 678
            },
            "averagePosition": 1,
            "bestPosition": 1,
            "worstPosition": 1
          },
          "..."
        ]
      }
    ],
    "market": {
      "competitorDensity": 4,
      "saturation": "medium",
      "marketState": "balanced",
      "opportunityAreas": [
        {
          "location": {
            "latitude": 47.60069099099099,
            "longitude": -122.3554629658123
          },
          "assessment": "The tracked business was not seen at this point among 0 nearby businesses.",
          "priority": "high"
        },
        "..."
      ],
      "reviewSynthesis": {
        "sentiment": "positive",
        "themes": [
          "store",
          "coffee",
          "starbucks"
        ],
        "reviewCount": 7171
      }
    },
    "marketReport": "The scan checked 9 of 9 grid points across 1 of 1 keywords. The market has medium saturation with 4.0 competing businesses per checked point. The opportunity findings are listed first.",
    "history": [
      {
        "checkedAt": "2026-09-30T23:22:32.967Z",
        "keyword": "Starbucks",
        "averagePosition": 2.4285714285714284,
        "rating": 3.8,
        "reviewCount": 448
      }
    ],
    "changeAlerts": "..."
  }
}
```

#### 💳 Pricing

This Actor uses pay-per-event pricing. The two events below are the charged result events for this Actor, and the current price is shown on the Store card and before a run.

| Buyer-facing event | Charged for |
| --- | --- |
| Organic position | One organic position for a submitted domain and keyword, including a not-found outcome within the checked depth. |
| Local position | One local-pack position for a submitted business, keyword, and grid point. |

#### 🔌 Integrations

Run the Actor from Apify Console, the API, a Task, or a Schedule. Read the dataset through the API, export it for reports, or use a webhook after a run to pass results to another workflow. Repeat runs can provide history and change alerts when prior data is available.

https://www.youtube.com/watch?v=bNACk1\_S\_6w\&list=PLObrtcm1Kw6MUrlLNDbK9QRg8VDJg0gOW\&index=4

#### ❓ FAQ

##### What does a not-found position mean?

It means the tracked domain was not seen within the organic depth, or the tracked business was not seen at that local grid point. It is different from a successful position and is represented by null where the schema allows it.

##### What happens when Google returns incomplete results?

Read accessStatus for each keyword or grid point and the run-level coverage fields. A partial status tells you that the requested depth or grid coverage was not fully returned.

##### Can I check several keywords in one run?

Yes. Add several values to keywords. They share the same country, language, and result-family settings, so one run cannot give each keyword its own separate configuration.

##### Can I track local rankings?

Yes. Choose Local rank tracking, add a business name, and set a scan center, radius, and grid resolution. The output includes the business position at each point, local visibility measures, and observed competitors.

##### Does this use Google Search Console?

No. It reads public Google Search and Google Maps results. It does not access private Search Console, private analytics, or other logged-in ranking data.

##### Can repeat runs show movement?

When prior checks are available, organic rows can include history and movement alerts, and local rows can include history and change alerts. Set the organic movement threshold to choose how much movement creates an organic alert.

##### How do country and language affect a run?

They set the Google context for every check in the run, and both values are saved on each dataset row. Use them when the market or language you want to check differs from the defaults.

##### How does this compare with Google Search Console, Semrush, or Ahrefs?

This Actor checks public Google results at run time and keeps source result context. It does not replace private campaign history, private Search Console data, or private SEO dashboards.

##### How is billing counted?

Billing uses one event for each organic domain position result and one event for each local business position at a grid point. See the Pricing section for the two buyer-facing events.

### 📝 Changelog

**v0.0** (01-10-2026)

- Initial release.

### 🆘 Support

For issues, questions, or feature requests, [file a ticket](https://console.apify.com/actors/maximedupre~google-rank-tracking/issues) and I'll fix or implement it in less than 24h 🫡

### 🔗 Related Actors

- [Ubersuggest Scraper](https://apify.com/maximedupre/ubersuggest-scraper) - Compare public ranking keywords, top pages, and SERP data alongside rank checks.
- [Google Rank & SERP Competitor Monitor](https://apify.com/feeng/google-rank-competitor-monitor) - Monitor Google organic positions and competitor result pages alongside this Actor.
- [Google Maps Scraper — Rank Tracking & Market Intelligence](https://apify.com/ryanclinton/google-maps-scraper) - Search local listings and market signals to complement grid checks.
- [Google Search Scraper: $1.50/1k SERP pages](https://apify.com/santamaria-automations/google-search-scraper) - Collect broader Google SERP pages when you need raw search results beyond tracked domains.
- [Google Maps Geo-Grid Local Rank Tracker](https://apify.com/fayoussef/geo-grid-local-rank-tracker) - Compare local grid rank views for location-focused SEO work.

**Made with ❤️ by Maxime Dupré**

# Actor input Schema

## `resultType` (type: `string`):

Choose which result family this run returns. Organic rank tracking checks a domain in Google Search. Local rank tracking checks a business across a Google Maps grid.

## `websiteDomain` (type: `string`):

Enter the domain to track, without https:// or a path. This field is required for Organic rank tracking.

## `organicDepth` (type: `integer`):

Set how many organic results to check for each keyword. A not-found result means the domain was not seen within this depth.

## `organicMovementThreshold` (type: `integer`):

Flag an organic change when a repeat check moves by this many positions or more. Use 1 to flag any position change.

## `localBusinessName` (type: `string`):

Enter the public business name to find in Google Maps. This field is required for Local rank tracking.

## `localBusinessAddress` (type: `string`):

Add the business address to help match the right Google Maps listing. Leave it blank when the name is enough.

## `localScanCenter` (type: `object`):

Set the center of the local grid with latitude and longitude in decimal degrees. Example: {"latitude": 40.7128, "longitude": -74.0060}.

## `localScanRadiusKm` (type: `number`):

Set how far the grid reaches from the scan center, in kilometers.

## `localGridResolution` (type: `string`):

Choose how many points each side of the local grid has. A 5 by 5 grid checks 25 points.

## `localPrioritization` (type: `string`):

Choose what the local market report puts first: opportunity areas, local visibility, or competition.

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

Enter one or more search terms. Organic rank tracking checks the domain for each keyword. Local rank tracking checks the business at each grid point for each keyword.

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

Enter a two-letter ISO 3166-1 country code, such as US. This sets the Google country context for every check.

## `languageCode` (type: `string`):

Enter a language code such as en or fr. Use a language-region code such as en-US when needed.

## Actor input object example

```json
{
  "resultType": "organic",
  "websiteDomain": "example.com",
  "organicDepth": 100,
  "organicMovementThreshold": 3,
  "localScanRadiusKm": 5,
  "localGridResolution": "5x5",
  "localPrioritization": "opportunity",
  "keywords": [
    "seo tools"
  ],
  "countryCode": "US",
  "languageCode": "en"
}
```

# Actor output Schema

## `results` (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 = {
    "resultType": "organic",
    "websiteDomain": "example.com",
    "organicDepth": 100,
    "organicMovementThreshold": 3,
    "keywords": [
        "seo tools"
    ],
    "countryCode": "US",
    "languageCode": "en"
};

// Run the Actor and wait for it to finish
const run = await client.actor("maximedupre/google-rank-tracking").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 = {
    "resultType": "organic",
    "websiteDomain": "example.com",
    "organicDepth": 100,
    "organicMovementThreshold": 3,
    "keywords": ["seo tools"],
    "countryCode": "US",
    "languageCode": "en",
}

# Run the Actor and wait for it to finish
run = client.actor("maximedupre/google-rank-tracking").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 '{
  "resultType": "organic",
  "websiteDomain": "example.com",
  "organicDepth": 100,
  "organicMovementThreshold": 3,
  "keywords": [
    "seo tools"
  ],
  "countryCode": "US",
  "languageCode": "en"
}' |
apify call maximedupre/google-rank-tracking --silent --output-dataset

```

## MCP server setup

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

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/nRCK18zjRLRypFyyP/builds/mWFTZQ1ae7xj6Pu9w/openapi.json
