The Cheapest Local Rank Tracking API for Developers

A national rank is an average of places you do not serve. Ask /api/search city by city for the number each customer sees.

Get an API Key View Documentation
Local pack listings as JSON 112 countries Google · Bing · Yahoo · DuckDuckGo · Brave From $0.03/1K calls Free to start

No per-location and no per-keyword fee — there is no list to register. You loop the places, you pay for the calls. Adding a 41st city costs one more call per keyword per run, not a plan upgrade.

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.

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.

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.

LeverHow you send itWhat 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.

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.

cURL — one place, one call
# 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"
Python — the same keyword across four towns
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.
JSON response (annotated)
{
  "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 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.

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.

Multi-location brand

40 stores, 25 keywords

$0.90
per month at Scale, tracked daily

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
See pricing →
Franchise & marketplace

Coverage, not vanity

1 call
per keyword, per place, per run

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
Geo-grid guide →

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.

WHEN A NAMED PLACE IS NOT ENOUGH

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.

See the Maps API →
{
  "places": [
    {
      "rank": 1,
      "name": "Bell Plumbing",
      "rating": 4.9,
      "review_count": 231,
      "coordinates": {
        "lat": 30.5083,
        "lng": -97.6789
      }
    }
  ]
}

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.

q
Required. The keyword, with the city or suburb in it for local intent — emergency plumber round rock tx. This is the field that carries the place.
country
Two-letter code, 112 accepted, default 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.
language
Two-letter ISO 639-1 code (en, es, fr…) for the result language. Anything that is not two letters is a 400.
engine
google, bing, yahoo, ddg or brave. One response shape across all five, so a five-engine sweep is one code path.
num
Organic results wanted, 1 to 100 (default 10). On /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.
pages
Result pages to walk, 1 to 10, used only when num is absent — if you send both, num wins. Positions stay sequential across pages.
freshness
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.
safe
off, moderate or strict. Anything else is a 400.
format
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.
pixel_position
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.
results.localPack[]
Response field. The pack listings — name, rating, reviews, category, address, hours. Always present, empty when the page carried no pack.
organic[].position
Response field. 1-indexed rank on every organic result, on every engine, with no opt-in. This is the primitive a local rank tracker is built on.

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 placeYesYesGrid rank, not organic
Local pack listings as fieldsYesUsually in-dashboard onlyYes
Coordinate-level gridsVia the Maps APIRarelyCore feature
Engines coveredFiveUsually Google onlyGoogle only
Billing shapePer API callPer location × per keywordPer grid scan
Adding a locationOne more callPlan changeMore credits
Raw rows in your databaseYesExport, if offeredExport, if offered
Free to tryFree to startLimited trialTrial 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.

Local rank tracking API questions

It is a search API you call once per place instead of once per keyword. A national rank tracker answers "where do I rank for emergency plumber". A local one answers "where do I rank for emergency plumber in Round Rock, and in Cedar Park, and in Georgetown" - three different result pages, three different competitor sets, three different numbers. With Serpent you send a location-qualified query to /api/search with a country target, read the 1-indexed position field on every organic result, and store one row per place per day.
Two controls, and we would rather be plain about the boundary between them. 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.
Yes. Every /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.
Yes, because there is nothing to register. Serpent has no concept of a tracked keyword or a tracked location - there is no list to maintain and no per-row subscription. You loop the places you care about and pay for the calls you make. Adding a 41st city costs one more call per keyword per run, not a plan upgrade, which is the structural reason multi-location tracking is cheap here and expensive in a managed tool.
25 keywords times 40 locations is 1,000 calls per daily run, so about 30,000 calls a month. That is $18.00 a month at the Default rate of $0.60 per 1,000, $1.80 at Growth ($0.06 per 1,000) and $0.90 at Scale ($0.03 per 1,000). Growth is unlocked by a one-time $100 deposit and Scale by a one-time $500 deposit; a deposit is spendable balance, not a fee. Switch the same job to weekly and it is roughly 4,300 calls a month.
Five engines - 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.
Deep Search, /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.
Same endpoint, different question. Rank tracking measures one domain against one query over time - the axis is dates. Local rank tracking measures one query across many places on the same day - the axis is geography. Most local SEO teams end up running both: a geographic sweep to find the weak markets, then a daily time series on the keywords and cities that matter.

Local rank tracking guides & tutorials

Walkthroughs that use the same position and localPack fields to build real per-place trackers.

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