An automated rank tracker should record what a requested search result page returned at a specific time. It should not pretend to know every user’s ranking, traffic, or future visibility. The useful unit of data is a local result record: query, country, language, retrieval time, and observed position within the returned page.
This tutorial uses the SearchCans Google Search API to build that record in Python. The code calls the documented endpoint, reads the data.organic response array, and saves an explicit result when the target domain is not present on the returned page.
Decide what the tracker will measure
Define the tracker before you schedule it. For each keyword, record:
- the exact query;
- the requested country and language;
- the target hostname;
- the retrieval time in UTC;
- the requested SERP page;
- the observed position, or an explicit not-observed result.
These fields make later comparisons useful. A position change for us and en is not the same measurement as a result for another country or language. The SearchCans geo parameters page lists supported values before you expand the keyword list.
Install the small dependency set
Use Python 3.10 or newer and install requests in your project environment:
pip install requests
Store your API key outside the source file. The example reads it from an environment variable named SEARCHCANS_API_KEY.
export SEARCHCANS_API_KEY="your_api_key"
On Windows PowerShell, set the environment variable for the current session with $env:SEARCHCANS_API_KEY = "your_api_key". Do not commit API keys to a repository or include them in an article screenshot.
Call the API and save a result
The example below tracks one target hostname for one local query. It does not invent a fallback position when the domain is missing. A missing result on the returned page is different from a guaranteed rank outside that page.
import json
import os
from datetime import datetime, timezone
from urllib.parse import urlparse
import requests
API_URL = "https://www.searchcans.com/api/v1/search"
def observe_keyword(query, country, language, target_hostname, page=1):
api_key = os.environ["SEARCHCANS_API_KEY"]
payload = {
"t": "google",
"s": query,
"country": country,
"language": language,
"p": page,
}
response = requests.post(
API_URL,
headers={"Authorization": f"Bearer {api_key}"},
json=payload,
timeout=30,
)
response.raise_for_status()
body = response.json()
if body.get("code") != 0:
raise RuntimeError(f"SearchCans returned code={body.get('code')}")
organic = body.get("data", {}).get("organic", [])
target = target_hostname.casefold().removeprefix("www.")
matches = []
for item in organic:
hostname = urlparse(item.get("link", "")).hostname or ""
normalized_host = hostname.casefold().removeprefix("www.")
if normalized_host == target or normalized_host.endswith("." + target):
matches.append(item)
observed_at = datetime.now(timezone.utc).isoformat()
record = {
"observed_at": observed_at,
"query": query,
"country": country,
"language": language,
"requested_page": page,
"target_hostname": target_hostname,
"api_response_id": body.get("data", {}).get("id"),
"result": "not_observed_within_returned_page",
"observed_position": None,
"matched_url": None,
"matched_title": None,
}
if matches:
best_match = min(matches, key=lambda item: item.get("position", 10**9))
record.update(
{
"result": "observed",
"observed_position": best_match.get("position"),
"matched_url": best_match.get("link"),
"matched_title": best_match.get("title"),
}
)
return record
record = observe_keyword(
query="research workflow for AI agents",
country="us",
language="en",
target_hostname="www.example.com",
)
print(json.dumps(record, indent=2))
The response format follows the documented data.organic structure. Each organic item includes fields such as title, link, snippet, source, and position. Test the code with a controlled query before you place it in a scheduled job.
Save history without overstating the result
Append every run to a JSON Lines file, a database table, or a governed spreadsheet. Keep the original fields rather than storing only one position number. A later analysis needs to know whether a change came from the locale, target page, query, time, or matching rule.
For example, these two records mean different things:
observed_position: 4means the target appeared at position four in the returned response.not_observed_within_returned_pagemeans the target did not appear in that response. It does not prove an absolute rank beyond the requested page.
This wording protects the team from a common reporting error. It also makes anomaly reviews easier when a query has multiple valid target URLs or the search result page changes shape.
Use country and language as part of the key
Never compare raw positions from different market settings as if they were the same series. A durable identifier can combine the normalized query, country, language, target hostname, and requested page.
For a first release, track a small keyword set in one locale. Once the records are stable, add another country-language pair as a separate series. This keeps reporting clear and avoids mixing market changes with setup errors.
Add an account-aware stop rule
Scheduled tracking can grow quickly. Before a large batch, query the SearchCans Account API for the current balance, concurrent lanes, and key status. Compare the result with the planned number of requests.
If capacity is insufficient, postpone the run or reduce the batch and label it as incomplete. Do not fill missed days with synthetic values. A gap in the series is better evidence than a made-up record.
Turn observations into an SEO review
Position tracking is a prompt for investigation, not an optimization instruction by itself. When a meaningful change appears, review the result page, inspect the matching URL, and read the page before deciding what to change. The SearchCans Reader SEO Audit Skill can help turn selected URLs into a readable audit queue.
Focus the review on the page’s actual usefulness: does it answer the query, explain the subject clearly, keep its claims current, and connect readers to the next relevant action? Avoid claims that a script, a schema change, or a single keyword edit will guarantee a particular result.
Frequently asked questions
Q: Is an observed position the same as a user’s rank?
A: No. The record describes the returned API response for a particular query, country, language, page, and time. Personalized results and future searches can differ.
Q: What should happen when the target domain is not returned?
A: Store an explicit not-observed result and preserve the request context. Do not convert it into a guessed position.
Q: Can this tracker monitor more than one country?
A: Yes. Treat each country and language pair as a separate measurement series, then compare them only with matching context.