How to Build a Rank Tracker with Google Search API in Python
Web Data Collection Specialist
TL;DR:
- A rank tracker is a time-series pipeline, not one SERP request. It must preserve query, location, language, device, search domain, observation time, ranking URL, and position.
- Use a Search API when you need raw SERP observations. Use a full SEO platform when you also need keyword discovery, reporting, alerts, and client workflows.
- Normalize hostnames before matching a target domain. Treat
www.example.com, scheme differences, paths, and subdomains according to an explicit policy. - Record “not ranked” as data. Do not convert absence into position zero, and do not carry yesterday's position forward.
- Store the ranking URL as well as the rank. A domain can keep its position while the landing page changes.
- Scrapeless Google Search API returns structured search data. The Python implementation below requests SERPs, parses organic results, and writes append-only CSV snapshots.
What a Rank Tracker Actually Measures
A rank tracker observes where a target domain appears in a specific search result set at a specific time.
That definition is deliberately narrow. The position is conditional on the query, country or location, language, Google domain, device, result type, and pagination depth. Change one dimension and the observation belongs to a different series.
A trustworthy record should contain at least:
- keyword
- target domain
- observed position or an explicit not-ranked state
- ranking URL
- result title
- country or location setting
- language
- device
- Google domain
- observation timestamp
The tracker should never imply that a sampled SERP is a universal rank. Personalization, experiments, index changes, and regional differences are part of search.
Search API vs Rank-Tracking Platform
A Search API and a rank-tracking platform solve related but different jobs.
| Need | Search API | Rank-tracking platform |
|---|---|---|
| Raw organic result records | Strong fit | Usually available through the platform model |
| Custom matching logic | Full control | Depends on platform |
| Own database and dashboard | You build it | Often included |
| Keyword discovery | Separate workflow | Often included |
| White-label reports | You build them | Often included |
| Unusual sampling schedule | Full control | Plan-dependent |
| Integration with internal data | Direct | Export or API-dependent |
Choose an API when rank observations are one input to an internal product, experiment, or data warehouse. Choose a platform when analysts need a ready-made interface and reporting workflow.
Google Search API Request Model
Scrapeless Google Search API accepts search parameters and returns structured data. The current Google Search API documentation documents common parameters including query, country, language, Google domain, result type, offset, and result count.
Use the current customer-facing name, Google Search API, in product copy. Stable actor names and API routes may retain older internal naming.
The request used by this guide is a POST to /api/v1/scraper/request with actor scraper.google.search. Authentication belongs in the x-api-token header. The input keeps the query and search settings together so every response can be traced to its sampling configuration.
Build the Tracker in Python
The script below does four jobs:
- sends one Google Search API request per keyword;
- finds the first organic result whose hostname matches the target policy;
- writes an append-only CSV snapshot;
- preserves a blank position and URL when the target is not found.
Prerequisite: the live request requires a Scrapeless API key in
SCRAPELESS_API_KEY. The matching, normalization, and CSV logic can be tested locally with a saved response fixture before making an authenticated request.
python
import csv
import os
from datetime import datetime, timezone
from pathlib import Path
from urllib.parse import urlparse, urlunsplit
import requests
API_URL = "https://api.scrapeless.com/api/v1/scraper/request"
def normalized_host(value: str) -> str:
candidate = value if "://" in value else urlunsplit(("https", value, "", "", ""))
host = (urlparse(candidate).hostname or "").lower().rstrip(".")
return host.removeprefix("www.")
def host_matches(result_url: str, target_domain: str, include_subdomains=True) -> bool:
result_host = normalized_host(result_url)
target_host = normalized_host(target_domain)
if not result_host or not target_host:
return False
return result_host == target_host or (
include_subdomains and result_host.endswith(f".{target_host}")
)
def organic_results(payload: dict) -> list[dict]:
if isinstance(payload.get("organic_results"), list):
return payload["organic_results"]
data = payload.get("data", {})
if isinstance(data, dict) and isinstance(data.get("organic_results"), list):
return data["organic_results"]
return []
def find_rank(payload: dict, target_domain: str) -> dict:
for fallback_position, item in enumerate(organic_results(payload), start=1):
url = item.get("link") or item.get("url") or ""
if host_matches(url, target_domain):
return {
"position": item.get("position", fallback_position),
"ranking_url": url,
"title": item.get("title", ""),
}
return {"position": None, "ranking_url": "", "title": ""}
def fetch_serp(keyword: str, *, gl="us", hl="en", device="desktop") -> dict:
api_key = os.environ["SCRAPELESS_API_KEY"]
response = requests.post(
API_URL,
headers={"x-api-token": api_key, "Content-Type": "application/json"},
json={
"actor": "scraper.google.search",
"input": {
"q": keyword,
"gl": gl,
"hl": hl,
"google_domain": "google.com",
"device": device,
"start": 0,
},
},
timeout=60,
)
response.raise_for_status()
return response.json()
def append_snapshot(path: Path, row: dict) -> None:
fields = [
"observed_at", "keyword", "target_domain", "position",
"ranking_url", "title", "gl", "hl", "device", "google_domain",
]
exists = path.exists()
with path.open("a", newline="", encoding="utf-8") as handle:
writer = csv.DictWriter(handle, fieldnames=fields)
if not exists:
writer.writeheader()
writer.writerow(row)
def track(keyword: str, target_domain: str, output="rank_history.csv") -> dict:
settings = {"gl": "us", "hl": "en", "device": "desktop"}
payload = fetch_serp(keyword, **settings)
match = find_rank(payload, target_domain)
row = {
"observed_at": datetime.now(timezone.utc).isoformat(),
"keyword": keyword,
"target_domain": normalized_host(target_domain),
"position": match["position"] or "",
"ranking_url": match["ranking_url"],
"title": match["title"],
**settings,
"google_domain": "google.com",
}
append_snapshot(Path(output), row)
return row
if __name__ == "__main__":
print(track("web scraping api", "scrapeless.com"))
The standard-library urlparse documentation explains why hostname parsing should use a URL parser instead of string slicing. The script removes only a leading www. and optionally accepts subdomains; adjust that policy before tracking a multi-brand domain estate.
Validate the Position Parser
Before using live credits, save one real API response from the account and run the parser against it. Include at least these fixtures:
| Fixture | Expected result |
|---|---|
| Exact apex domain | Matched |
www. version |
Matched |
| Allowed subdomain | Matched |
Lookalike domain such as example.com.attacker.test |
Not matched |
| Malformed or missing result URL | Not matched |
| Target absent from sampled pages | Position is blank; state is not ranked |
Do not calculate a position from list order when the API supplies an explicit position field without first understanding pagination. On a later results page, list index one is not global position one. Preserve the supplied position or add the page offset deliberately.
Store History Without Rewriting It
Append-only snapshots are easier to audit than one mutable “current rank” table. A later transformation can select the newest row per keyword and market.
CSV works for a personal tracker. A production service should use a database key that distinguishes keyword, domain, country or location, language, device, Google domain, and observation time. The SQLite table documentation is enough for a compact local service; a warehouse becomes useful when the series feeds dashboards and alerts. For URL identity rules beyond the hostname policy used here, consult the URI generic syntax standard.
Keep both position and ranking_url. These changes mean different things:
- position changes, URL unchanged: the same landing page moved;
- position unchanged, URL changes: Google selected a different page;
- position blank: the domain was not found within the sampled result depth;
- several URLs from the domain appear: store the best position and optionally retain every match in a detail table.
Handle Geo, Language, Device, and Time
Treat search settings as dimensions, not optional labels added later.
glindicates a country context.hlcontrols interface language.google_domainselects the Google property.deviceseparates desktop and mobile observations when supported.- a precise location setting can model a market more narrowly than a country.
- the timestamp should use UTC in storage and convert only for display.
Do not mix a city-level series with a country-level series under the same chart line. Likewise, a mobile result should not silently replace a desktop observation.
Sampling time also matters. Run comparable keyword groups in one bounded window. If a batch spans many hours, store the timestamp of each request rather than one date for the entire job.
Calculate Cost Without Publishing a Stale Price
The stable calculation is more useful than a copied plan amount:
monthly requests = keywords × markets × devices × pages sampled × runs per month
Then apply the current account rate and failure-handling policy. Separate planned requests from repeated and failed attempts so an operations team can explain the invoice. Check Scrapeless pricing at implementation time rather than embedding a number that may age before the code does.
Start Scraping with Scrapeless
Power up your web scraping and automation workflow with Scrapeless!
Sign up today and get $5 in free credit — no credit card required.Claim your free credit now in the Scrapeless Dashboard.
Production Checklist
- Pin a schema contract for the fields the parser consumes.
- Keep the API key in a secret manager or environment variable.
- Repeat only bounded requests after temporary service failures; do not loop indefinitely.
- Store request settings beside every observation.
- Distinguish not ranked from request failed.
- Track the ranking URL, not only the numeric position.
- Preserve one redacted response fixture for parser regression tests.
- Respect applicable terms, privacy requirements, and local law.
- Alert on missing batches and schema drift before alerting on SEO movement.
Conclusion
A useful rank tracker is a disciplined observation system. Scrapeless Google Search API supplies structured SERP records; the value comes from explicit matching, complete search dimensions, append-only history, and honest treatment of absent results.
Start with one keyword, one market, one device, and one verified fixture. Once the series is stable, expand the batch and connect the CSV or database to a dashboard. For adjacent workflows, see the Google Search API guide.
Build Your First SERP Snapshot
Join the Scrapeless community for implementation help and data-pipeline patterns: Discord · Telegram.
Create a free account at app.scrapeless.com, run one bounded query, and validate the saved response before scheduling the tracker.
FAQ
Q: What is a rank tracker API?
A rank tracker API provides search result or rank observations that software can store and analyze. A SERP API returns raw result records; a dedicated rank-tracking API may also provide projects, history, alerts, and reports.
Q: How do I find my domain's position in organic results?
Parse each organic result URL with a URL parser, normalize the hostname, apply an explicit apex/subdomain policy, and return the first matching result's supplied position. Avoid substring matching.
Q: What position should I store when the domain is missing?
Store a null or blank position plus an explicit not-ranked state for the sampled depth. Do not use zero and do not carry forward the previous observation.
Q: How often should a rank tracker run?
Choose a cadence based on the decision the data supports. Daily sampling is common for active SEO monitoring, while slower strategic reporting may need less. Consistency and comparable settings matter more than maximum frequency.
Q: Does this script prove a universal Google ranking?
No. It records one structured observation for defined query, market, language, device, domain, depth, and time settings. Search results can vary outside that sampling configuration.
At Scrapeless, we only access publicly available data while strictly complying with applicable laws, regulations, and website privacy policies. The content in this blog is for demonstration purposes only and does not involve any illegal or infringing activities. We make no guarantees and disclaim all liability for the use of information from this blog or third-party links. Before engaging in any scraping activities, consult your legal advisor and review the target website's terms of service or obtain the necessary permissions.


