Local SEO Rank Tracking via API: Multi-Location Tutorial (2026)

By Anurag Pathak· · 10 min read

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:

  1. Market — country and language. 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-letter language code to set the result language.
  2. 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:

  1. 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.
  2. 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:

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

  1. 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.
  2. Mixing English queries with non-English markets. Set language explicitly. The same keyword in English vs. the local language returns different results.
  3. Reading only the top 10. Pull num=100 so you catch competitors and your own listings below the fold.
  4. Sampling at the wrong time. Results shift during business-hours peaks. Run sweeps at the same hour each week.
  5. Tracking one engine only. Run the same sweep with engine=bing / yahoo / ddg if 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.

Get Your Free API Key

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.