Local rank tracking API: track Google local rankings by city with code
Build a local rank tracker with an API: find a business's position in Google's local results for any city, handle chains, and log the history. Python.
CrawlerData teamPublished 8 min read
Contents
A local rank is a position in a list: where does this business appear when someone searches dentist in austin tx? It is the number local SEO teams report to clients, and it is easy to get wrong, because local results change from one request to the next and many businesses are chains with the same name in every city.
This guide builds a local rank tracker with the Google Local API. We cover how location works, a find_local_rank() function that handles chains and ads, a table that stores the history, and what it costs. Every result below comes from requests we ran in October 2026. Local results change often, so yours will differ.
How location works
The google-local endpoint takes a query, a country, a language and an optional location. There are two ways to point a search at a city, and they are not interchangeable:
- City in the query text.
dentist in austin txanddentist in round rock txare two different searches. - The
locationparameter. A Google location name that ends with a country, such asAustin,Texas,United States, with a short query likedentist.
To find valid location names, use the public lookup. It needs no API key:
curl "https://api.crawlerdata.com/public/locations?q=austin"
For austin it returned Austin,Texas,United States, Austintown,Ohio,United States and Austin,Minnesota,United States, each with its type (City) and country. A name that is not valid is rejected with a 400 that tells you to use a Google canonical location name ending with a supported country.
The two approaches give different lists. Joe's Pizza on Bedford Ave ranked 3 for the query pizza in brooklyn ny, but 30 for the query pizza with location set to Brooklyn,New York,United States. Neither is wrong. They are different searches. Choose one approach for a business and keep using it, or you will compare ranks that are not comparable.
To track a business, choose the searches that matter to it, such as the service and the city or neighbourhood a customer would type, and track each one separately.
Find a business’s rank
Put your key in an environment variable:
export CRAWLERDATA_API_KEY="YOUR_API_KEY"
import os
import re
import requests
API_URL = "https://api.crawlerdata.com/v1/jobs"
HEADERS = {"Authorization": f"Bearer {os.environ['CRAWLERDATA_API_KEY']}"}
def local_results(query, country="us", language="en", page=1, location=None):
params = {"query": query, "country": country, "language": language, "page": page}
if location:
params["location"] = location # e.g. "Austin,Texas,United States"
body = {"endpoint": "google-local", "params": params, "queue": "live"}
resp = requests.post(API_URL, json=body, headers=HEADERS, timeout=180)
resp.raise_for_status()
data = resp.json()
if data["job"]["status"] != "completed":
raise RuntimeError(data["job"]["error"])
return data["result"]["local_results"]
def norm(name):
"""'Joe's Pizza & Pasta' -> 'joes pizza and pasta'"""
name = name.lower().replace("&", " and ").replace("’", "").replace("'", "")
return re.sub(r"[^a-z0-9]+", " ", name).strip()
def find_local_rank(business, query, address_contains=None, max_pages=2, **kwargs):
"""Where `business` ranks in Google's local results for `query` (ads not counted).
Chains share a name, so pass part of the street address in `address_contains`
to be sure you matched the right branch."""
target, rank = norm(business), 0
for page in range(1, max_pages + 1):
results = local_results(query, page=page, **kwargs)
if not results:
break
for r in results:
if r.get("sponsored"):
continue
rank += 1
name_ok = target in norm(r["title"]) or norm(r["title"]) in target
address_ok = not address_contains or address_contains.lower() in (r["address"] or "").lower()
if name_ok and address_ok:
return {"rank": rank, "title": r["title"], "rating": r["rating"],
"reviews": r["reviews"], "address": r["address"], "pages_checked": page}
return {"rank": None, "title": None, "rating": None, "reviews": None, "address": None, "pages_checked": max_pages}
Three details in that function matter:
- It counts only organic results. Each result has a
sponsoredflag, and the loop skips sponsored ones so the rank is the organic position. - It keeps its own running rank across pages. A page holds about 20 businesses, and if the business is not on page 1 the function reads page 2 and keeps counting.
- It matches on a normalised name. Apostrophes, ampersands and punctuation differ between a client’s spelling and Google’s, so both sides are cleaned before they are compared.
Results from five real checks
We ran five checks for two businesses:
ATX Family Dental | dentist in austin tx | rank 1 | 1700 S 1st St, Austin, TX 78704 | 4.9★ 916 reviews
ATX Family Dental | emergency dentist in austin tx | rank 11 | 1700 S 1st St, Austin, TX 78704 | 4.9★ 916 reviews
ATX Family Dental | dentist in round rock tx | not found in 2 pages (about 40 businesses)
Joe's Pizza | pizza in brooklyn ny | rank 3 | 216 Bedford Ave, Brooklyn, NY 11249 | 4.5★ 3600 reviews
Joe's Pizza | pizza in manhattan ny | rank 1 | 1435 Broadway, New York, NY 10018 | 4.4★ 27000 reviews
The same dentist was first for one search, eleventh for a close variant and absent for the neighbouring city. That is why you track searches, not a business: the rank belongs to the combination.
The same check with location
find_local_rank() passes extra arguments through, so location works without changing it. We ran five more checks, this time with a short query and location set:
find_local_rank("ATX Family Dental", "dentist", location="Austin,Texas,United States")
ATX Family Dental | 'dentist' @ Austin,Texas,United States | rank 1 | 1700 S 1st St, Austin, TX 78704
ATX Family Dental | 'dentist' @ Round Rock,Texas,United States | not found in 2 pages
ATX Family Dental | 'emergency dentist' @ Austin,Texas,United States | rank 3 | 1700 S 1st St, Austin, TX 78704
Joe's Pizza | 'pizza' @ Brooklyn,New York,United States | rank 30 | 216 Bedford Ave, Brooklyn, NY 11249 (page 2)
Joe's Pizza | 'pizza' @ New York,New York,United States | rank 1 | 124 Fulton St, New York, NY 10038
The dentist is first for dentist in Austin and absent in Round Rock, as before. emergency dentist was rank 11 as a query that included the city and rank 3 with location set, another sign that the two approaches are different searches. And in New York the function found a different Joe’s Pizza branch, on Fulton St, at rank 1.
Chains and branches
Joe's Pizza has branches in both boroughs, with different addresses, ratings and review counts, and a name-only match returns whichever branch Google lists first. If you are tracking one location, pass part of its street address:
find_local_rank("Joe's Pizza", "pizza in brooklyn ny", address_contains="Bedford Ave")
With address_contains="Bedford Ave" the function returned the Brooklyn branch at rank 3. Asked for a Broadway address in the same Brooklyn search, it found nothing in two pages, which is the correct answer, because that branch is a Manhattan one.
Some results have no address. In a German test search, the first result had address, phone, hours and website all empty. If you track businesses in a market like that, match on the name alone and check each match by hand the first time.
Local results move
Run a check twice and you may get two answers. In our testing, a search for pizza in brooklyn ny listed Joe’s Pizza first in one request, and a later request put it third. We did not isolate why, and you do not need to know to handle it.
So treat one check as a sample. Check each search once or more a day, store every result and look at the trend over a week or two, the same advice that applies to ranking in the organic results.
Store the history
A tracker is a table. Store the date, the business, the search, the rank, and the rating and review count at the time, because those move too:
import sqlite3
from datetime import date
def init_db(path="local_ranks.db"):
db = sqlite3.connect(path)
db.execute(
"""CREATE TABLE IF NOT EXISTS local_ranks (
day TEXT, business TEXT, query TEXT, rank INTEGER, rating REAL, reviews INTEGER,
PRIMARY KEY (day, business, query))"""
)
return db
def record(db, business, query, **kwargs):
r = find_local_rank(business, query, **kwargs)
db.execute(
"INSERT OR REPLACE INTO local_ranks VALUES (?, ?, ?, ?, ?, ?)",
(date.today().isoformat(), business, query, r["rank"], r["rating"], r["reviews"]),
)
db.commit()
return r
A NULL rank means the business was not in the pages you checked that day. That is different from a rank of zero, so keep it as null when you chart it. Because rating and review count are stored on every row, you can also see when a rank drop lines up with a competitor’s review count climbing. The review counts are the numbers our Google reviews guide explains, including that counts above 1,000 look rounded.
What it costs
Every request is 1 credit, on every queue. A local search returns about 20 businesses per page, so a business found on page 1 costs 1 credit and one found on page 2 costs 2. Starter is $1.00 per 1,000 requests and the largest pack is $0.30 per 1,000, see pricing.
| What you track | Checks a month | Credits if found on page 1 | At $1.00 per 1,000 | At $0.30 per 1,000 |
|---|---|---|---|---|
| 20 businesses, 2 searches each, daily | 1,200 | 1,200 | $1.20 | $0.36 |
| 50 businesses, 3 searches each, daily | 4,500 | 4,500 | $4.50 | $1.35 |
| 200 businesses, 3 searches each, daily | 18,000 | 18,000 | $18.00 | $5.40 |
If a business has to be searched to page 2, double the credits. When many businesses share a search, such as 30 dentists for dentist in austin tx, fetch the search once and look up every business in the same results, instead of paying for the same search 30 times. Failed requests are not charged.
Next steps
To see a search the way someone in a given city sees it, our free Google results by city checker runs without an account.
You now have a function that finds a business’s position for any search and city, handles chains and ads, and a table that records the history. The Google Local API page lists every parameter with a full example response, and Build a local business lead list from Google uses the same endpoint to find the businesses in the first place. For the other half of local data, see Google Reviews API, and for the full range of use cases, local business data.
Frequently asked questions
- What is a local rank tracker?
- A tool that records where a business appears in Google's local results for a search such as dentist in austin tx, and how that changes over time. This guide builds one in Python: a function that finds the business's position, plus a table that stores every check.
- How do I get the city into the search?
- Two ways. Pass location, a Google location name such as Austin,Texas,United States, or put the city in the query text, such as dentist in austin tx. Both work on the google-local endpoint, but they return different lists, so pick one and use it for every check of a business.
- How do I avoid matching the wrong branch of a chain?
- Match the name and part of the street address. The function in this guide takes an address_contains argument. In our test, Joe's Pizza had a branch on Bedford Ave in Brooklyn and one on Broadway in Manhattan, with 3,600 and 27,000 reviews.
- Do I get the same ranking every time I check?
- No. Local results move between requests. In our test a business was first in one request and third in a later one for the same search. Record a rank every day and read the trend, not a single check.
- Are sponsored listings counted in the rank?
- Not in this guide's function. Each result has a sponsored flag, and the code skips sponsored results so the rank counts only organic local results.
- How much does local rank tracking cost?
- 1 credit per page of results, and a page has about 20 businesses. Tracking 50 businesses for 3 searches each, daily, is 4,500 checks a month: $4.50 at the Starter rate of $1.00 per 1,000 requests if each is found on page 1, and double if you need page 2.
- Local rank tracking
- Local rank tracker
- Google Local API
- Local SEO
- Python
- Tutorial