Local SEO Rank Tracking via API: Multi-Location Tutorial (2026)
Local SEO is geo-specific. A dental practice in Austin ranks differently for "best dentist" depending on which part of town the searcher is in. A single national rank-tracker reading does not capture that. To track local rankings honestly you have to check each location you serve and aggregate.
This tutorial shows how to do that with a SERP API in Python. We build a list of the locations a business serves, run a location-qualified query for each, read the position off the organic results, and roll the data up into a per-location visibility view your client can actually look at.
Building this with an API? The Local Rank Tracking API returns a 1-indexed position on every result for any keyword and target location — the data layer behind everything in this guide.
How Location Targeting Works Here
There are two levers you combine to get a location-specific ranking:
- Market —
countryandlanguage. Pass a two-letter country code (us,gb,de,in, and 50+ more) so Google serves the results a searcher in that market sees, and a two-letterlanguagecode to set the result language. - Locality — in the query itself. Qualify the keyword with the place you are checking:
"best dentist austin","best dentist round rock","best dentist cedar park". Each query returns the ranking for that locality's intent.
Put those together and one keyword across a list of localities gives you a visibility map of where you are strong and where you fall off — the same picture a coordinate grid produces, built from queries that work on any standard SERP API.
The API Call
Send the location-qualified keyword to /api/search with your market target. Every organic result carries a 1-indexed position.
import requests
API_KEY = "YOUR_API_KEY"
def rank_in_location(keyword, locality, domain, country="us", language="en"):
"""Position of `domain` for `keyword` in a given locality, or None."""
q = f"{keyword} {locality}"
r = requests.get(
"https://apiserpent.com/api/search",
headers={"X-API-Key": API_KEY},
params={"q": q, "engine": "google",
"country": country, "language": language, "num": 100},
timeout=30,
)
for item in r.json()["results"]["organic"]:
if domain in item["url"]:
return item["position"]
return None # not in top 100
# Example: where does the practice rank for "best dentist" in downtown Austin?
print(rank_in_location("best dentist", "austin downtown", "smithfamilydentalatx.com"))
Each item in results.organic is a clean, structured result you can read directly:
{
"position": 1,
"title": "Smith Family Dental — Downtown Austin",
"url": "https://smithfamilydentalatx.com",
"snippet": "Family and cosmetic dentistry in downtown Austin …",
"displayedUrl": "smithfamilydentalatx.com"
}
Building the Location List
Instead of a grid of raw coordinates, work from the localities your business actually serves — the neighbourhoods, suburbs, and nearby towns where your customers are. That is what a local searcher's query carries anyway.
# The places this business wants to be found in
locations = [
"austin downtown", "south congress austin", "round rock",
"cedar park", "pflugerville", "georgetown", "leander",
"west lake hills", "buda", "kyle",
]
print(f"{len(locations)} locations to check")
For a typical service business, 10 to 40 locations covers a metro area well. Franchises and multi-location brands keep one list per branch and union them.
Running the Sweep
Sequential calls are fine for a handful of locations; for larger lists use asyncio + httpx with a semaphore set to your tier's concurrent cap.
import asyncio
import httpx
SEMAPHORE = asyncio.Semaphore(8) # tune to your tier
async def fetch_one(client, keyword, locality, domain):
async with SEMAPHORE:
r = await client.get(
"https://apiserpent.com/api/search",
params={"q": f"{keyword} {locality}", "engine": "google",
"country": "us", "language": "en", "num": 100},
headers={"X-API-Key": API_KEY}, timeout=30,
)
organic = r.json().get("results", {}).get("organic", [])
pos = next((x["position"] for x in organic if domain in x["url"]), None)
return {"locality": locality, "position": pos}
async def sweep(keyword, domain, locations):
async with httpx.AsyncClient() as client:
tasks = [fetch_one(client, keyword, loc, domain) for loc in locations]
return await asyncio.gather(*tasks, return_exceptions=True)
scores = asyncio.run(sweep("best dentist", "smithfamilydentalatx.com", locations))
for s in scores:
print(s["locality"], s["position"] or "not ranked")
Now scores is a list of (locality, position) rows — your visibility across the whole service area in one pass.
Turning Scores into a Visibility View
Two easy options:
- Table or bar chart. Sort localities by position. Reds (no top-10 presence) jump out instantly — those are the areas to target with content, citations, and reviews.
- Map. Geocode each locality name to a centre point (any geocoder works) and drop a colour-coded marker per location. Output is a self-contained HTML map you can drop into a client report.
import folium
# Lower position is better; convert to a 0-10 weight for colouring
GEO = { # locality -> (lat, lng), from your geocoder, computed once
"austin downtown": (30.2672, -97.7431),
"round rock": (30.5083, -97.6789),
# … one entry per locality
}
m = folium.Map(location=[30.2672, -97.7431], zoom_start=10)
for s in scores:
if s["locality"] not in GEO:
continue
pos = s["position"]
colour = "green" if pos and pos <= 3 else "orange" if pos and pos <= 10 else "red"
folium.CircleMarker(
GEO[s["locality"]], radius=10, color=colour, fill=True,
popup=f"{s['locality']}: {pos or 'not ranked'}",
).add_to(m)
m.save("local_visibility.html")
Open the HTML in a browser. Green markers are where you own the top of the results; red markers are the gaps to work on.
Persisting Snapshots Over Time
One sweep is interesting; weekly sweeps are actionable. Add SQLite storage and a date column:
import sqlite3
from datetime import date
conn = sqlite3.connect("local_rank.db")
conn.execute("""
CREATE TABLE IF NOT EXISTS snapshots (
snap_date TEXT, keyword TEXT,
locality TEXT, position INTEGER,
PRIMARY KEY (snap_date, keyword, locality)
)
""")
today = date.today().isoformat()
for s in scores:
conn.execute(
"INSERT OR REPLACE INTO snapshots VALUES (?, ?, ?, ?)",
(today, "best dentist", s["locality"], s["position"]),
)
conn.commit()
To compare two sweeps and find where you gained or lost positions, JOIN the table on (keyword, locality) across two snap_date values and compute the delta.
The Cost
Three things drive cost: number of locations, keyword count, and refresh cadence. For a typical local business:
- 1 keyword × 20 locations × weekly = 80 queries/month
- 10 keywords × 20 locations × weekly = 800 queries/month
- 50 keywords (multi-location franchise) × 20 locations × weekly = 4,000 queries/month
At Serpent API Scale tier ($0.30 per 1,000 quick searches): the first row is under $0.03/month, the second about $0.24/month, the third about $1.20/month. Even the franchise setup costs less than a single seat in most managed local trackers. See full pricing.
Common Pitfalls
- Location list too coarse. One query for a whole metro hides real visibility gaps. Break a city into its neighbourhoods and suburbs to see where you actually drop off.
- Mixing English queries with non-English markets. Set
languageexplicitly. The same keyword in English vs. the local language returns different results. - Reading only the top 10. Pull
num=100so you catch competitors and your own listings below the fold. - Sampling at the wrong time. Results shift during business-hours peaks. Run sweeps at the same hour each week.
- Tracking one engine only. Run the same sweep with
engine=bing/yahoo/ddgif your audience uses them.
Build Your Local Visibility Map
Serpent API returns a 1-indexed position on every Google result, localizes by country and language, and works on any location-qualified query — with flat per-call pricing from $0.30 per 1,000 queries at Scale tier. 10 free searches with every new account.
Explore: Local Rank Tracking API · Playground · Local SEO ranking guide
FAQ
How do I track local rankings via API?
Send a location-qualified query — the keyword plus the place you serve, like "emergency plumber austin" — with a country and language target. Every organic result returns a 1-indexed position, so you read off where your site ranks for that keyword in that location.
How do I track many locations at once?
Keep a list of the localities you serve and run the same keyword as a location-qualified query for each, storing the position per locality and date. The result is a per-location ranking matrix you can refresh on a schedule.
Can I set the country and language?
Yes. Pass country with a two-letter ISO code and language with a two-letter code. That sets the market and language; the locality in the query handles the city- or neighbourhood-level intent.
How often should I re-track local rankings?
Weekly for most service businesses. Local results change more slowly than national organic because the geographic-relevance signal dominates. Daily is overkill outside competitive niches like personal injury law and dental in major metros.
Can I track competitors with the same setup?
Yes. Change the matching condition in the sweep to look for a competitor's domain instead of your own. Run the same per-location sweep and you have their visibility map.


