Try it
Run the same query from two different places
One real call, no signup and no key. Run plumber near me, then name a city and run it again — the ranked list changes.
What the box will and will not show you. This demo runs on a free account, so it returns one page of organic results with their position, plus related searches; the paid feature blocks — the local pack among them — come back withheld. The panel names each one it is holding back. The localPack array shown further down this page is what the same request returns on a key of your own.
The idea
What local rank data is, and why teams use it
Local search does not have one answer per keyword. It has one answer per place, and the gap between them is where the money is.
A national rank is a blended number nobody sees
“We rank #4 for emergency plumber” is not a fact about any customer. It is an average over markets, and averages hide the shape: strong in the city you started in, invisible three suburbs out, where the second van sits idle.
The fix is not a better average. It is one measurement per place, stored per place, so the weak markets can be named.
The local pack moves the whole page
When a query carries local intent, a block of business listings is injected near the top of the result page. Organic position 1 is then pushed down it — the same ordinal rank, a very different amount of attention.
So local tracking has two axes: your ordinal position, and whether a pack sat above it. Serpent returns both in one response.
Your competitors change at the county line
Nationally you are up against national brands. Twenty miles away you are up against three firms nobody outside that town has heard of. A per-place result set is the only way to see who you are actually losing to.
Pull the full list per place and you get the competitor set, not just your own number.
Location is what makes the bill explode
Managed local tools price a subscription per location and per tracked keyword, so cost multiplies exactly where local SEO gets interesting — the 40th store, the 300th keyword.
An API priced per call inverts that. Geography stops being a billing decision and goes back to being a coverage decision.
Location targeting
The four ways to move the search location
Every local rank tracker is built on some version of these. Here is exactly which ones are request parameters and which one is not, because the difference matters when you are designing a sweep.
| Lever | How you send it | What it changes |
|---|---|---|
| Country | country=us |
A real request parameter, and the coarsest lever. 112 two-letter codes, uk and gb both accepted; GET /api/countries lists them. This is the one to loop when you sell in several national markets rather than several suburbs. |
| City or suburb | q=emergency plumber round rock tx |
The place goes in the query text. There is no city parameter on /api/search — and rather than dress that up, it is worth saying that a location-qualified query is what most local rank tracking measures anyway, because it is what a searcher who wants a local answer actually types. |
| Language | language=es |
Two-letter ISO 639-1 code, which is a separate axis from country. Bilingual markets rank differently in each language, so a Miami or Montreal sweep usually wants both. |
| A point on the map | /api/maps/search?lat=&lng= |
When you need the ranking at a coordinate rather than for a named place — a geo-grid — that is a different endpoint. The Maps API takes lat/lng (or ll) plus zoom, and returns a rank on every place it finds. |
Two honest boundaries. First: a location-qualified query returns the page for that query in that country, which is what a named-place rank tracker measures — it is not the same thing as physically standing on that street corner, and any provider claiming otherwise is making the same trade-off with better marketing. Second: the Maps endpoint is a different price class — $0.015 per call at Default, $0.0105 at Scale, against $0.0006 and $0.00003 for a SERP call — so use coordinates where you need them and named places everywhere else. See full pricing.
Worked example
One keyword, four towns, one matrix
This is the shape of every local rank tracker: an outer loop over places, an inner read of position, one row stored per place per run. Nothing here needs a special flag — position ships on every organic result.
# The city lives in the query; the market lives in `country`. curl "https://apiserpent.com/api/search?q=emergency+plumber+round+rock+tx&engine=google&country=us&num=20" \ -H "X-API-Key: YOUR_API_KEY"
import requests API = "https://apiserpent.com/api/search" HEAD = {"X-API-Key": "YOUR_API_KEY"} SERVICE = "emergency plumber" DOMAIN = "yoursite.com" # The service area, not the country list. This is the axis that matters. PLACES = ["austin tx", "round rock tx", "cedar park tx", "georgetown tx"] def local_rank(place): r = requests.get(API, headers=HEAD, timeout=60, params={ "q": f"{SERVICE} {place}", # the place goes in the query "engine": "google", "country": "us", # the market "num": 20, }).json() organic = r["results"]["organic"] # `position` is 1-indexed and present on every row, on every engine. rank = next((x["position"] for x in organic if DOMAIN in x["url"]), None) # `localPack` is always present — empty when the page carried no pack. pack = [b["name"] for b in r["results"]["localPack"]] return rank, pack for place in PLACES: rank, pack = local_rank(place) print(place, rank or "not in top 20", "| pack:", pack[:3]) # austin tx 3 | pack: ['Radiant Plumbing', 'S&D Plumbing', 'Reliant Plumbing'] # round rock tx 11 | pack: ['Bell Plumbing', 'Aus-Tex Services', 'Mattingly Plumbing'] # cedar park tx not in top 20 | pack: [] # georgetown tx 7 | pack: ['Alpha Plumbing', 'Longhorn Plumbing', 'Mint Plumbing'] # # Four numbers, one keyword. The national average of those is 7 — # a number that describes none of the four towns you sell in.
{
"success": true,
"query": "emergency plumber round rock tx",
"engine": "google",
"country": "us",
"results": {
"localPack": [ // [] when the page carried no pack
{
"name": "Bell Plumbing",
"rating": "4.9",
"reviews": "231",
"category": "Plumber",
"address": "1200 N Mays St, Round Rock",
"hours": "Open 24 hours"
}
],
"organic": [
{
"position": 11, // 1-indexed, always present
"title": "…",
"url": "https://yoursite.com/round-rock",
"snippet": "…",
"pixel_position": 1480 // only with pixel_position=true
}
]
},
"meta": { "totalOrganic": 20 }
}
Values are illustrative. The field names, their types and the fact that localPack and position are always present are read from the endpoint’s own response contract — run the box above, or the cURL line, to see a live one.
The local pack
The block that decides local search, parsed
For a local-intent query the pack is the result. Serpent hands you its listings as fields rather than leaving you to parse a rendered page.
Six fields on every listing
- localPack[].name
- localPack[].rating
- localPack[].reviews
- localPack[].category
- localPack[].address
- localPack[].hours
Enough to answer the three questions a local report is built on: am I in the pack, who is, and how far ahead of me are they on reviews.
Always a key, sometimes a pack
results.localPack is on every /api/search response. It is an empty array when the query had no local intent, or when the page simply carried no pack that run — the key never disappears, so your parser needs no guard and your schema never changes.
Branch on len(results["localPack"]), not on the key existing.
Deep Search is where the pack lives
/api/search carries the feature blocks. /api/search/quick is organic-only by design — the leaner, faster call for plain position tracking, with no feature blocks at all.
Both draw on the same web-search rate card, so pick on what you need back rather than on cost.
A pack does not make you rank worse
It makes rank mean less. Organic #1 under a pack can sit a full screen further down than organic #1 without one, so a position that looks flat week to week can be losing clicks.
That is what pixel_position measures — the same result, in pixels from the top rather than ordinals.
Who buys this
Three teams, one shape of problem
Each of them is usually paying a per-location fee somewhere. The figures below are the call volume of each job, priced at the published rates.
40 stores, 25 keywords
One sweep a day over every trade area, so a store that slips out of the pack is a Tuesday alert rather than a quarterly surprise.
- 1,000 calls per daily run
- ~30,000 calls a month
- $18.00/month at Default, $1.80 at Growth
- Weekly instead: ~4,300 calls
60 clients, 3 towns each
Managed tools bill you per client location and hand back a dashboard with their name on it. Here the rows land in your database and the report carries yours.
- No per-location subscription line
- White-label the whole output
- Same call shape on all five engines
- Onboard a client without a plan change
Coverage, not vanity
Franchisors and marketplaces care which territories are covered at all. A per-place matrix answers that directly; a national rank cannot.
- Score share of places in the top 3
- Name the franchisee losing the pack
- Compare territories on one scale
- Add a market for one more call
Rates are the published web-search rates: $0.60 per 1,000 calls at Default, $0.06 at Growth (one-time $100 deposit) and $0.03 at Scale (one-time $500 deposit). A deposit is spendable balance, not a fee. One thing to know before you budget a trial: on /api/search, an account still on the Default rate with a balance under $10 is billed per result page rather than per call, so a num=100 call there counts as ten. Above that balance, and on Growth and Scale, it is one charge per call however many pages it walks — and /api/search/quick is one charge per call for everyone.
Geo-grids need a coordinate, and that is a different endpoint
A geo-grid asks the same question from dozens of points laid over one city, then colours the map by rank. Named places cannot express that — a grid point is a latitude and a longitude, not a suburb.
/api/maps/search takes lat and lng (or the ll shorthand from a Google Maps URL) plus a zoom from 1 to 21, and returns up to 20 places on the quick route, each with a rank, coordinates, rating, review_count, phone and website. Walk your grid, keep the rank per point, and you have Share of Local Voice.
{
"places": [
{
"rank": 1,
"name": "Bell Plumbing",
"rating": 4.9,
"review_count": 231,
"coordinates": {
"lat": 30.5083,
"lng": -97.6789
}
}
]
}
Parameters
Everything you can control
Read from the endpoint itself, not from a feature list. Every row below is a parameter /api/search actually acts on.
emergency plumber round rock tx. This is the field that carries the place.us. Both uk and gb work. Omit it and you get us; send a code we do not accept and you get a 400 naming the parameter, not a quiet substitution. Full list at GET /api/countries.en, es, fr…) for the result language. Anything that is not two letters is a 400.google, bing, yahoo, ddg or brave. One response shape across all five, so a five-engine sweep is one code path./api/search it rounds up to a whole page, so num=15 asks for 20. Pull 20 or more per place: a local competitor set is wider than a page.num is absent — if you send both, num wins. Positions stay sequential across pages.h, d, w, m, y (or 1h, 1d, 7d, 1m, 1y). Rarely useful for local rank work, where you want the standing page rather than the recent one.off, moderate or strict. Anything else is a 400.full (default) for every block including localPack, or simple for position, title and URL only — a smaller payload when you are storing thousands of rows a night.true on /api/search adds a pixel offset and a pixel_box to every item, so you can see how far a pack pushed you down. Paid plans, desktop measurement, and the pixel position page lists which engines carry it. Sent to /api/search/quick the search still runs normally and the response says pixelPositionUnavailable: "not_supported_on_quick" with a hint naming the right endpoint, rather than failing.name, rating, reviews, category, address, hours. Always present, empty when the page carried no pack.Compare
API vs. managed local tools
Capability only, no prices — a fair price row would need every vendor re-checked on the day you read this. The structural differences below do not move.
| Capability | Serpent API | Managed local rank trackers | Geo-grid tools |
|---|---|---|---|
| Ordinal position per place | Yes | Yes | Grid rank, not organic |
| Local pack listings as fields | Yes | Usually in-dashboard only | Yes |
| Coordinate-level grids | Via the Maps API | Rarely | Core feature |
| Engines covered | Five | Usually Google only | Google only |
| Billing shape | Per API call | Per location × per keyword | Per grid scan |
| Adding a location | One more call | Plan change | More credits |
| Raw rows in your database | Yes | Export, if offered | Export, if offered |
| Free to try | Free to start | Limited trial | Trial credits |
Tracking a domain over time rather than across places? That is the rank tracking API. Measuring where a result physically sits on the page? That is pixel position. All three are the same endpoint asked a different question.
FAQ
Local rank tracking API questions
/api/search with a country target, read the 1-indexed position field on every organic result, and store one row per place per day.
country sets the market - 112 two-letter codes, and it is a real request parameter. The city, suburb or neighbourhood goes in the query text itself: q=emergency plumber round rock tx. There is no city parameter on /api/search, and any provider offering one is choosing the same trade-off under a different name. If you need a ranking measured at a specific latitude and longitude rather than for a named place, that is the Maps API, which takes lat and lng directly.
/api/search response carries a localPack array. When the result page showed a pack of business listings, each entry gives you name, rating, reviews, category, address and hours. When the query had no local intent, or the page carried no pack, the array is empty - the key is always there, so your parser never needs a guard. Rich blocks like this one are a Deep Search product: /api/search/quick deliberately returns organic results only. Read results.localPack.length rather than assuming a pack exists.
engine=google, engine=bing, engine=yahoo, engine=ddg and engine=brave - and 112 country codes, including both uk and gb. Every engine returns the same response shape with a position field on every organic result, so one code path covers all five. Country codes are listed by GET /api/countries.
/api/search, is the right endpoint for local work: it is the one that carries the localPack array and the other feature blocks, and it is the one that accepts pixel_position. Quick Search, /api/search/quick, returns organic results only by design - it is the faster, leaner call for plain position tracking. Both cost the same, so the choice is about what you need back, not about budget.
Build it yourself
Local rank tracking guides & tutorials
Walkthroughs that use the same position and localPack fields to build real per-place trackers.
Local SEO Rank Tracking
The overview: what to measure per place, how the local pack changes the picture, and how to report it to a client.
Local Rank Tracking in Python
The loop from this page, built out: parse position, store a row per place per day, and diff the runs.
Local Rank Tracking API Tutorial
Hands-on: pull rankings per location, capture local pack visibility, and produce a weekly diff report.
Geo-Grid Local Rank Tracking
How grid scanning works, how to compute Share of Local Voice, and what a coordinate buys you over a place name.
International Rank Tracking
The same sweep on the country axis instead of the city axis, across 40+ markets side by side.
AEO for Local Businesses
How local businesses earn visibility in AI answers, and how to measure that alongside your per-place rankings.
Start tracking rankings place by place
One keyword, one position per city, the local pack alongside it. Per-call pricing from $0.03/1K calls, free API calls, no subscription and no per-location fee.
Get an API Key