Google Bing Yahoo DuckDuckGo Brave

Rank Tracker API — Build Trackers, Reports & Dashboards on Live Position Data

Every organic result comes back with a 1-indexed position. Store it with a date and you have a rank history, priced per call.

Get an API Key View Documentation
Position on every organic result Google · Bing · Yahoo · DuckDuckGo · Brave 112 countries From $0.03/1K calls Free to start

The endpoint a rank log runs on is GET /api/search/quick. It is billed flat — one call, one charge, whatever depth you ask for, on every tier and at any balance. You are billed per call, never per tracked keyword.

Check a live ranking right now

One real call, no signup and no key. The panel opens on Quick Search — read the position on each row, beside the JSON.

What ranking data is, and who buys it

A rank is not a metric a search engine publishes. It is an observation: one query, one place, one moment, and the ordered list that came back. Everything a rank tracker sells is built out of repeating that observation and keeping the answers.

Agencies who have to show the work

An agency's monthly report lives or dies on a position column. What they need is not a prettier chart — it is data they own, on their own keywords, in their own brand, with a history nobody can revoke when a subscription lapses.

Per-call pricing is the part that changes their margin: a client with 400 keywords costs what 400 checks cost, not what a 500-keyword plan costs.

Product and SEO teams inside a company

In-house teams already have a warehouse, a scheduler and a BI tool. They do not want a twelfth dashboard login; they want a table of positions arriving next to their traffic and revenue tables so the join is possible at all.

That is an API-shaped job: one GET, a position on every row, and a schema they choose.

People building the tracker itself

Every rank tracker on the market is a UI over somebody's position data. If you are building one, this is the layer underneath — and the reason to buy it rather than scrape it is that the response shape never changes and the failure cases are already named.

The same five engines, the same keys, the same types, on every call.

Agents and models that need today's ranking

A language model cannot know where a page ranks this morning; that fact did not exist when it was trained. A rank call gives an agent a dated, checkable answer — position, title and URL — instead of a plausible guess.

format=simple trims each row to exactly those three fields, which is usually all a model needs in context.

THE FIELD THAT MATTERS

One field, on every row, on every engine, with no flag to set

position is a 1-indexed integer on every organic result. It is not opt-in, it is not a paid add-on, and it does not change name or type between engines — so the code that reads a Google rank reads a Brave rank unchanged.

It is contiguous: the list is numbered from 1 with no gaps, across every page walked, so row 47 in the array is position 47 in the response. And it is relative to the set you asked for — which is why the depth you request, and whether you actually received it, is the thing a rank log has to record alongside the number.

GET /api/search/quick
  ?q=best+running+shoes
  &engine=google
  &country=us
  &num=50

{
  "position": 7,
  "title": "Best Running Shoes 2026",
  "url": "https://example.com/shoes",
  "snippet": "We tested 41 pairs...",
  "displayedUrl": "example.com"
}

Quick Search for the log, Deep Search for the audit

Both return the same position on the same organic rows. They differ in what else comes back, in how num is read, and — the part that decides a tracking budget — in how a multi-page call is billed.

What differs Quick Search — /api/search/quick Deep Search — /api/search
Billing Flat. One call, one charge, whatever depth you ask for — on every tier and at any balance. One charge per call on Growth and Scale, and on a Default account holding $10 or more. Below that, a multi-page call is billed per page.
num Taken as given, 1100. num=25 asks for 25. Rounded up to the next 10 and read in a 10100 band. num=25 becomes 30.
pages 110, used when num is not set. 110, used when num is not set. Defaults to 1 page.
What else is in the response The ranked organic list. Every feature key is still present in results so the shape never varies; a block this call does not carry arrives as [] or null. The rest of the page as named fields: ads, People Also Ask, related searches, featured snippet, knowledge panel, local pack, inline videos, rich snippets.
pixel_position Not supported. The parameter is accepted, the search runs normally, and the response says so in metadata.pixelPositionUnavailable with a hint naming the other endpoint. Supported on google, yahoo, bing and ddg, on paid tiers, at the same per-call price.
Typical use The recurring log. Every keyword, every day, at the lowest cost per check. The occasional audit: what was above you on the page, and how far down the page you actually sat.

Both endpoints are free-tier eligible and both draw from the same pool of free API calls. include_aio, the AI Overview opt-in, is read only on Deep Search. pixel_position is not offered on engine=brave on either endpoint — see the pixel position API for what it measures and where.

How to actually build a daily rank log

Five decisions separate a rank log you can trust from a table of numbers that quietly lies to you. None of them is about the HTTP call — that part is one line.

Pick the row key before you pick the schema

A rank is only meaningful with its context attached. The unit is one keyword, one engine, one country, one day — make that the primary key and a re-run overwrites cleanly instead of double-counting.

Store position, the URL that ranked, and the date. The URL matters more than people expect: a keyword that "held position 4" while the ranking page changed underneath it is a different story than one that held with the same URL.

Record the depth you scanned, not just the rank you found

This is the one people get wrong. "Not found" is not a fact about your ranking — it is a fact about your window. A domain absent from 50 scanned rows and a domain absent from 100 are different observations, and a chart that plots both as the same blank is a chart that invents drops.

Write the row count you actually looked at into the same row. Then "fell out of the top 50" is a query you can answer, and "we stopped looking" never masquerades as a ranking change.

Treat a short delivery as its own outcome

Ask for a depth and receive less, and the response carries a top-level delivery block: requested, returned, a plain-English note, sometimes a reason, and where a job form is live an async_endpoint. A call that named no depth carries no block at all — with no target there is nothing to be short of.

So the test is simply is there a delivery key. If there is, you looked at a smaller page than you asked for, and the honest thing to store is a gap rather than a null that reads as a drop.

Ask for the smallest window that answers your question

num=50 and below delivers in full; above that a call is best-effort. Most tracking questions are top-20 questions, and a shallower window is both more reliable and faster. Go deep when you are hunting for a page that is genuinely far down, not by default.

On Quick Search the depth does not change the price — one call is one charge — so depth is purely a reliability and latency decision, not a budget one.

Send format=simple and stop parsing what you throw away

format=simple returns a flat array of exactly position, title and url — three columns, which is a rank log's whole schema. No snippet text, no feature blocks, nothing to strip before the insert.

Same price, same call, a payload a fraction of the size. Use format=full on the audit run when you want the rest of the page.

Check a ranking in one call, log it in twenty lines

A rank check is a GET with an API key header. No SDK, no job to poll, no id to chase — the positions are in the response body when it returns.

cURL — three rank calls worth copying
# 1. Where does this keyword rank on Google in the US, top 50?
curl "https://apiserpent.com/api/search/quick?q=best+running+shoes&engine=google&country=us&num=50" \
  -H "X-API-Key: YOUR_API_KEY"

# 2. The same keyword, three columns only — position, title, url
curl "https://apiserpent.com/api/search/quick?q=best+running+shoes&engine=google&num=20&format=simple" \
  -H "X-API-Key: YOUR_API_KEY"

# 3. The same keyword in a second market, in its own language
curl "https://apiserpent.com/api/search/quick?q=laufschuhe+test&engine=google&country=de&language=de&num=20" \
  -H "X-API-Key: YOUR_API_KEY"
JSON Response (format=simple, shortened)
{
  "success": true,
  "query": "best running shoes",
  "type": "web",
  "engine": "google",
  "country": "us",
  "pagesScraped": 5,
  "results": [
    { "position": 1, "title": "", "url": "https://example.com/a" },
    { "position": 2, "title": "", "url": "https://example.com/b" },
    { "position": 3, "title": "", "url": "https://example.com/c" }
  ],
  "meta": {
    "total": 50,             // rows you actually got — store this
    "requestedNum": 50,
    "elapsed": "2841ms",
    "timestamp": "2026-09-07T09:12:04.000Z"
  }
}

// Had this call come back short, it would also carry a top-level block:
//   "delivery": { "requested": 100, "returned": 63, "note": "…" }
// A full answer has no `delivery` key at all.
Python — a daily rank log with the two traps handled
import os, sqlite3, datetime, requests

API    = "https://apiserpent.com/api/search/quick"
KEY    = os.environ["SERPENT_API_KEY"]
DOMAIN = "yoursite.com"
DEPTH  = 50   # num=50 and below delivers in full

db = sqlite3.connect("ranks.db")
db.execute("""CREATE TABLE IF NOT EXISTS rank_log (
  day TEXT, keyword TEXT, engine TEXT, country TEXT,
  position INTEGER,   -- NULL = we looked and did not find it
  scanned  INTEGER,   -- how many rows we actually looked at
  short    INTEGER,   -- 1 = we got a smaller window than we asked for
  url      TEXT,
  PRIMARY KEY (day, keyword, engine, country))""")

def check(keyword, engine="google", country="us"):
    r = requests.get(API, headers={"X-API-Key": KEY}, params={
        "q": keyword, "engine": engine, "country": country,
        "num": DEPTH, "format": "simple"}, timeout=60)
    r.raise_for_status()
    body = r.json()

    rows    = body["results"]              # format=simple: a flat list
    scanned = body["meta"]["total"]
    # `delivery` is present ONLY when we named a depth and came up short.
    short   = bool(body.get("delivery"))

    hit = next((x for x in rows if DOMAIN in x["url"]), None)
    return {
        "position": hit["position"] if hit else None,
        "url":      hit["url"]      if hit else None,
        "scanned":  scanned,
        "short":    short,
    }

day = datetime.date.today().isoformat()
for line in open("keywords.txt"):
    kw = line.strip()
    if not kw: continue
    row = check(kw)
    # A missing domain in a SHORT window is not a drop — flag it, do not plot it.
    db.execute("INSERT OR REPLACE INTO rank_log VALUES (?,?,?,?,?,?,?,?)",
               (day, kw, "google", "us",
                row["position"], row["scanned"], int(row["short"]), row["url"]))
    db.commit()
Node.js — the same keyword on all five engines
const engines = ["google", "bing", "yahoo", "ddg", "brave"];

for (const engine of engines) {
  const url = `https://apiserpent.com/api/search/quick`
    + `?q=serp+api&engine=${engine}&country=us&num=20&format=simple`;
  const res  = await fetch(url, { headers: { "X-API-Key": process.env.SERPENT_API_KEY } });
  const body = await res.json();

  const hit = body.results.find(r => r.url.includes("yoursite.com"));
  // Say WHICH kind of "no" it was — the two mean different things.
  const verdict = hit          ? `#${hit.position}`
                : body.delivery ? `not in ${body.meta.total} rows (asked for 20)`
                :                 `not in top ${body.meta.total}`;
  console.log(engine.padEnd(7), verdict);
}

Everything a rank call can control

The same parameter set on /api/search/quick and /api/search, with the two differences called out. Anything not listed here is not read on these endpoints.

q
The keyword to look up. Required.
engine
google, bing, yahoo, ddg or brave. Defaults to google. Every engine returns the same keys with the same types, so a second engine is a query parameter rather than a second integration.
country
Two-letter code — us, gb, de, in and 112 codes in all; both uk and gb are accepted. Defaults to us. GET /api/countries returns the full list and needs no API key. Country-level only; city and postcode targeting are not supported here.
language
Two-letter ISO 639-1 code (en, es, de, ja…) to set the result language. Derived from country when omitted.
num
How many organic results to ask for, up to 100. Default 10. A ceiling you request, not a count that is guaranteed: num=50 and below delivers in full, above that is best-effort. On Quick Search the value is taken as given (1100); on Deep Search it is rounded up to the next 10 and read in a 10100 band.
pages
Number of result pages to walk, 110. Used only when num is not set — send one or the other, not both. Positions stay sequential across pages.
freshness
Restrict to recent pages: h or 1h, d or 1d, w or 7d, m or 1m, y or 1y.
safe
SafeSearch: off, moderate or strict.
format
full (default) returns the whole response object. simple returns a flat array of position, title and url per row — a rank log's entire schema, at the same price.
pixel_position
true on /api/search adds an integer pixel_position and a pixel_box (x, y, w, h) to every measured item. Paid tiers; available on google, yahoo, bing and ddg. On /api/search/quick the parameter is accepted and the search runs normally, but no pixel fields come back — the response carries metadata.pixelPositionUnavailable and a metadata.pixelPositionHint naming /api/search instead.
include_aio
The AI Overview opt-in, read on /api/search only, on eligible paid calls. Not read on Quick Search.
Response: position
A 1-indexed integer on every organic result, on every engine, with no opt-in. Contiguous from 1 across every page walked. This is the rank-tracking primitive.
Response: delivery
Present only when you named a depth and the call came back short of it: requested, returned, a plain-English note, sometimes a reason, and where a job form is live an async_endpoint. The same counts also appear as meta.partialResults.

Every parameter above is one the route actually reads. GET /api/status reports what your own key is entitled to, including which async job forms are live for your account — read that rather than a table before you build.

Per call, not per tracked keyword

Web search is one billing category: Quick and Deep, all five engines, one rate. A deposit is credit you spend rather than a monthly fee, and the tier it unlocks is permanent.

Default

Default

$0.60
/1K calls
  • No deposit and no subscription
  • Covered by the free API calls
  • Quick Search: one charge per call
  • Deep Search below a $10 balance bills per page
Per call$0.0006
Deposit neededNone
See pricing →
Scale

Scale

$0.03
/1K calls
  • Unlocked by a one-time $500 deposit
  • 20× off the Default rate
  • Always one charge per call
  • Same rate on all five engines
Per call$0.00003
Deposit needed$500 once
See pricing →

Worked example, one engine, one country, once a day: 2,000 keywords is about 60,000 calls a month — $1.80 at Scale, $3.60 at Growth, $36.00 at Default. Depth does not change that on Quick Search, because a Quick call is billed flat whatever depth you ask for. Growth and Scale accounts are billed once per call on Deep Search too; a Default account holding under $10 is the one case where a multi-page Deep call is billed per page.

What position data costs everywhere else

Ranking data is sold two ways: per call, like an API, or per tracked keyword, like a rank tracker. The second one charges you again for every keyword on the list whether or not anyone looked at it, which is where the two models separate.

Provider How it is priced Cost per 1,000 position checks
Serpent Per call. No plan, no per-seat fee, no per-keyword fee. $0.60 Default → $0.06 Growth → $0.03 Scale
SerpApi Monthly plan with a search allowance. $25.00 on Starter ($25/mo, 1,000) → $1.96 on their largest listed plan ($106,050/mo, 54M)
DataForSEO Per request, against a $50 minimum payment. $0.60 standard queue → $1.20 priority → $2.00 live
AccuRanker Per tracked keyword, refreshed daily. $3.73 — from $224/mo for 2,000 keywords
Semrush Suite subscription with a tracked-keyword allowance. $7.82 — from $117.33/mo (annual) for 500 keywords daily

Every rate is the vendor's own published price, read from their pricing page and checked 7 September 2026. The two per-keyword rows are converted at 30 daily refreshes a month so the column is comparable — that conversion is our arithmetic, not their published figure. On free access: SerpApi's free plan is 250 searches a month; DataForSEO lists no free tier and a $50 minimum payment; AccuRanker's pricing page lists no free plan or trial. Serpent gives every new account Free to start, with no card.

And the honest caveat. AccuRanker and Semrush are finished products — dashboards, stored history, share-of-voice, competitor discovery, alerting, support. The row above prices the data, not the product. If what you want is a tool you log into, buy the tool. If what you want is the positions, in your own database, on your own schedule, that is what an API is for.

Full breakdowns in our SERP API pricing comparison and rank tracking cost guides.

The details that only matter when you run it every day

Six behaviours that make the difference between a rank log you can chart and one you have to apologise for.

Contiguous positions

position is numbered from 1 with no gaps, across every page walked. Row 47 in the array is position 47 in the response — no re-indexing on your side.

A shape that never varies

Every feature key is always present in results. A block a call did not carry arrives as [] or null, never as a missing key, so one parser survives every engine.

Short answers say so

Ask for a depth and get less, and a delivery block names requested and returned. You are never left counting an array to work out whether a blank is a drop.

Depth that does not cost more

Quick Search is billed flat — one call, one charge, whatever depth you ask for. Going from num=20 to num=50 changes your latency, not your invoice.

112 countries, one code path

Change country and you are reading the ranking for that market. GET /api/countries lists every accepted code and needs no API key.

A three-column payload

format=simple returns exactly position, title and url. Same call, same price, nothing to strip before the insert.

When one keyword becomes two hundred

A loop over a keyword file is the right answer for a long time. Two things change when the list gets long, and both are parameters rather than rewrites.

Submit the batch instead of looping it

POST /api/bulk/jobs takes up to 200 items in one submission, each item the parameters of its synchronous twin. Use the serp_quick key for the Quick Search behaviour, or serp_web when you want the Deep Search depth. Poll the job, collect the results, and each item's result is the same body the synchronous call returns.

Each item is billed at the same flat rate for its category — asking for more results per item does not cost more. GET /api/status lists which job forms are live for your key under limits.endpoints[…].async_available; read that before you build. Full walkthrough in the documentation.

Spread the schedule, and never re-run blind

A thousand keywords do not need to leave at midnight together. Spread them across the window you have; a rank check is not more accurate for being simultaneous, and a staggered schedule is kinder to every part of the chain including yours.

When you re-run a day, write to the same (day, keyword, engine, country) key so the second answer replaces the first instead of appearing as a second observation. That one constraint is what keeps a history honest after an outage.

Rank Tracking API Questions

Send your keyword to /api/search/quick with engine=google and country=us. Every organic result in the response carries a 1-indexed position field. Find your domain in the results and read its position — that is its rank in the set you scanned. Run the same call on a schedule and store each position with the date to build a history. The position field is not opt-in; it ships on every organic result on every engine.
Quick Search, /api/search/quick, for the recurring log. It is billed flat: one call, one charge, whatever depth you ask for, on every tier and at any balance. Deep Search, /api/search, is billed one unit per page requested, up to 10 pages, at every tier and every balance — so the same num=100 request costs ten units on Deep and one on Quick, whatever the account. Use Deep Search when you also need the rest of the page: ads, People Also Ask, featured snippets, knowledge panels, or pixel positions. Use Quick Search when you only need the ranked list.
They are the same endpoint used two ways. A SERP or rank checker API answers "where does this keyword rank right now?" with a single on-demand call. A rank tracking API answers "how has this keyword moved?" by scheduling that same call on an interval and storing each result with its date. Serpent returns the position field on every search, so it works as both — a one-off rank checker and the data layer behind a continuous rank tracker.
Google, Bing, Yahoo, DuckDuckGo and Brave. Pass engine=google, engine=bing, engine=yahoo, engine=ddg or engine=brave. Every engine returns the same response shape with a position on every organic result, so tracking a keyword on a second engine is one query parameter rather than a second integration.
Yes. Pass country with a two-letter code — country=us, country=gb, country=de, country=in, and 112 codes in all; both uk and gb are accepted. GET /api/countries returns the full list and needs no API key. Pass language with a two-letter ISO 639-1 code to set the result language. Targeting is country-level: city and postcode targeting are not supported on this endpoint. For position as it varies by place, see the local rank tracking API.
Up to 100 organic results in a single call, set with num — but num is a ceiling you request, not a count that is guaranteed. num=50 and below delivers in full; above that a call is best-effort. When a call comes back short of the depth you named, the response carries a top-level delivery block with requested, returned, a plain-English note and, where a job form is live, an async_endpoint. A call that named no depth carries no block, because there is nothing to be short of. For a rank log that distinction matters: a domain missing from 62 scanned rows is not the same fact as a domain missing from 100.
Web search — Quick and Deep alike — is $0.60 per 1,000 calls on the Default tier, $0.06 per 1,000 on Growth after a one-time $100 deposit, and $0.03 per 1,000 on Scale after a one-time $500 deposit. A deposit is credit you spend, not a subscription, and the tier it unlocks is permanent. There is no per-seat fee and no per-tracked-keyword fee. Tracking 2,000 keywords once a day is about 60,000 calls a month: $1.80 at Scale, $3.60 at Growth, $36 at Default. Every new account gets Free to start. See full pricing.
On Deep Search. Add pixel_position=true to /api/search and every measured item also carries an integer pixel_position — its y-coordinate on the rendered desktop page — plus a pixel_box with x, y, width and height. It is available on engine=google, engine=yahoo, engine=bing and engine=ddg, on paid tiers, at the same per-call price. Quick Search does not support it: the parameter is accepted, the search runs normally, and the response says so in metadata.pixelPositionUnavailable with a hint naming /api/search, rather than quietly returning nothing. See the pixel position API.
A managed rank tracker is a finished product: a dashboard, stored history, share-of-voice charts, competitor discovery and alerting. This is the data layer underneath one. You get live, structured positions you own and can put in your own database, dashboard, agent or client report — and you build the rest. That is also why it is priced per call rather than per tracked keyword, which is what makes the arithmetic change at scale: a per-keyword tool charges you again for every keyword you add whether or not you looked at it, while a per-call API charges only for the checks you actually run.

Start tracking rankings free

free API calls, no card and no subscription. Positions on five engines and 112 countries, from $0.03 per 1,000 SERP API calls at Scale.

Get an API Key

Related guides

Walkthroughs that use the position field above to build something real.

Build a Keyword Rank TrackerThe end-to-end build: query, parse, store a daily history, chart the movement. Python Rank Tracker in 100 LinesA compact tracker you can put straight on a cron job. Bulk Keyword Rank CheckingHow to shape the job when the keyword list stops fitting in a loop. Google Dropped num=100 — What BrokeWhy deep rank data got harder in 2025, and what trackers did about it. International Rank TrackingOne keyword across 40+ countries, compared side by side. What Rank Tracking CostsPer-keyword maths for a per-call SERP API, at real volumes. No-Code Daily Tracker (n8n)A scheduled rank tracker without writing a backend. SerpApi AlternativesAn honest landscape of the SERP APIs a tracker can be built on. Free Google Search APIs, TestedWhat every free tier actually gives you while you prototype.