Back to BlogTutorial

How to Monitor Google AI Overviews for Your Keywords (API Tutorial)

September 29, 202612 min read

To monitor Google AI Overviews for your keywords, fetch each keyword's results page on a schedule, record whether an AI Overview appeared, which sources it cited and whether your domain is one of them, and store your organic rank from the same page beside it. With Searlo that is one request per keyword, GET /serp?q=...&aioverview=true, at 3 credits per results page with the AI Overview included. The Google AI Overview API page documents every field; this tutorial turns it into a daily tracker in Python.

Searlo is independent and is not affiliated with, endorsed by or sponsored by Google, OpenAI, Microsoft or Perplexity. Google publishes no official AI Overview API: Searlo renders the results page in a real browser, waits for the AI Overview to finish, and parses it.

What AI Overview tracking should measure

Four facts per keyword, market and day answer most questions about AI Overviews:

1. Presence: did Google show an AI Overview for this keyword in this country, language and device?

2. Sources: which domains did it cite, and in what order?

3. Your citation: is your domain among them, and at which source position?

4. Your organic rank: where does your page rank on the same results page?

Rank and citation have to be recorded together, because they have drifted apart. Ahrefs analysed 863,000 keyword results pages and 4 million AI Overview URLs (published March 2, 2026; checked 2026-09-29) and found that 38% of the pages cited in AI Overviews also ranked in the top 10 for the keyword, while 31.2% ranked between 11 and 100 and 31.0% ranked outside the top 100. Its July 2025 analysis had put the top-10 share at about 76%. A page-one ranking no longer implies a citation, and a citation no longer implies a ranking, so a tracker that stores only one of the two misses what changed.

The AI Overview tracking API call

One GET /serp request returns the AI Overview and the rest of the results page from the same snapshot. Listing 1 shows a trimmed response with real keys, illustrative values and placeholder domains; these are the keys a tracker reads:

KeyWhat it holds
aioverviewThe AI Overview as text, markdown and sources, or null when Google showed none
aioverview.sourcesEach citation: position, label, title, url, domain and supportingCount (the +N Google shows)
organicOrganic results with position, title, url, domain and snippet
adsAds from the top and bottom ad slots
peopleAlsoAskThe People Also Ask questions, as { question } objects
relatedSearchesRelated searches, as { query } objects
aiModeAvailableWhether Google offered its AI Mode tab (Searlo does not return AI Mode answers)
cached, fetchedAtWhether the page came from a short-lived cache, and when it was fetched

The parameters that matter are q, gl and hl (country and language), location or uule for a city, device (desktop, mobile or tablet) and aioverview=true. AI Overviews differ across all of them, so fix them per tracked market and store them with every row. Each call renders a real page and lets the AI Overview finish writing, so it takes seconds rather than milliseconds: Listing 2 allows 120 seconds for one page, and Listing 3 allows 600 seconds for a batch of 20. The People Also Ask API and Google SERP API pages cover the rest of the response.

The code is split into five numbered listings; each Python and shell listing starts with a comment that names its file.

json
{ "success": true, "searchParameters": { "q": "how do heat pumps work", "gl": "us", "hl": "en", "page": 1, "pages": 1, "num": 10, "device": "desktop", "type": "serp" }, "aioverview": { "text": "A heat pump moves heat instead of generating it…", "markdown": "A heat pump moves heat instead of generating it…", "sources": [ { "position": 1, "label": "Example Energy", "title": "How heat pumps work", "supportingCount": 2, "url": "https://www.example-energy.org/heat-pumps", "redirectUrl": null, "resolved": true, "domain": "example-energy.org" }, { "position": 2, "label": "Example", "title": "Heat pump buying guide", "supportingCount": 0, "url": "https://www.example.com/guides/heat-pumps", "redirectUrl": null, "resolved": true, "domain": "example.com" } ] }, "aiModeAvailable": true, "organic": [ { "position": 1, "title": "How does a heat pump work?", "url": "https://www.example-home.com/guides/heat-pump", "domain": "example-home.com", "snippet": "Heat pumps use refrigerant to carry heat…", "page": 1 }, { "position": 4, "title": "Heat pump buying guide", "url": "https://www.example.com/guides/heat-pumps", "domain": "example.com", "snippet": "What to check before you buy…", "page": 1 } ], "ads": [], "peopleAlsoAsk": [{ "question": "What is the downside of a heat pump?" }], "relatedSearches": [{ "query": "heat pump vs furnace" }], "fetchedAt": "2026-09-29T08:41:12.004Z", "cached": false }

Step 1: Check one keyword

Listing 2 answers the whole question for one keyword: is there an AI Overview, is your domain cited, and where do you rank on the same page. Match domains on the hostname and its subdomains; Searlo returns hostnames with only www. removed, so a citation of help.example.com counts for example.com. Google wraps some citation links in redirects, which Searlo resolves by default. A source it could not resolve comes back with resolved: false and an empty domain, and the listing skips it.

python
# Listing 2: check_one.py - one keyword: is there an AI Overview, am I cited, where do I rank? import requests API = "https://api.searlo.tech/api/v1" HEADERS = {"x-api-key": "YOUR_API_KEY"} MY_DOMAIN = "example.com" def mine(domain): # Hostnames come back with only "www." removed: count subdomains as yours. # An unresolved Google redirect has an empty domain and never matches. return bool(domain) and (domain == MY_DOMAIN or domain.endswith("." + MY_DOMAIN)) resp = requests.get( f"{API}/serp", params={"q": "how do heat pumps work", "gl": "us", "hl": "en", "aioverview": "true"}, headers=HEADERS, timeout=120, # the page is rendered and the AI Overview allowed to finish ) resp.raise_for_status() serp = resp.json() aio = serp.get("aioverview") # None when Google showed no AI Overview sources = aio["sources"] if aio else [] cited = [s["position"] for s in sources if mine(s.get("domain"))] ranked = [o["position"] for o in serp["organic"] if mine(o.get("domain"))] print("AI Overview shown:", aio is not None) print("Cited domains:", [s.get("domain") for s in sources]) print("My citation position:", cited[0] if cited else "not cited") print("My organic position:", ranked[0] if ranked else "not on page one")

Step 2: Run the whole keyword set through /serp/batch

POST /serp/batch takes up to 20 keywords per request and returns every results page in one response. Three details matter for AI Overview tracking:

• Ask for the AI Overview. The batch endpoint leaves it out unless the body says "aioverview": true.

• Billing is per distinct keyword. 3 credits per keyword per results page. A keyword repeated inside one batch is fetched and billed once, and credits.used in the response says what the batch cost.

• Partial success. One failed keyword does not fail the batch. Its row comes back with success: false, a message and a retryable flag, and it is billed, because the batch as a whole succeeded. Re-run the retryable ones; a failed single GET /serp request is refunded.

Listing 3 reads a keyword file, sends it 20 keywords at a time, and writes one row per keyword, market and day to SQLite: whether an AI Overview appeared, whether and where you were cited, your organic rank, the cited domains and the fetch time. For hundreds of keywords per call, queue them as async tasks instead, up to 500 per request; the Bulk SERP API page has that example.

python
# Listing 3: track.py - today's snapshot of every keyword, 20 per /serp/batch call, into SQLite import json import sqlite3 import time from datetime import date import requests API = "https://api.searlo.tech/api/v1" HEADERS = {"x-api-key": "YOUR_API_KEY"} MY_DOMAIN = "example.com" MARKET = {"gl": "us", "hl": "en", "device": "desktop"} # one tracked market # One keyword per line. Duplicates are dropped: every batch bills each keyword it fetches. with open("keywords.txt", encoding="utf-8") as f: KEYWORDS = list(dict.fromkeys(line.strip().lower() for line in f if line.strip())) def mine(domain): return bool(domain) and (domain == MY_DOMAIN or domain.endswith("." + MY_DOMAIN)) def run_batch(keywords): """Up to 20 keywords. The batch endpoint leaves the AI Overview off unless asked.""" for _ in range(3): r = requests.post( f"{API}/serp/batch", json={"keywords": keywords, "aioverview": True, **MARKET}, headers=HEADERS, timeout=600, # the pages are fetched a few at a time ) if r.status_code == 429: time.sleep(int(r.headers.get("retry-after", 60))) continue r.raise_for_status() return r.json() raise RuntimeError("batch refused three times; try again later") db = sqlite3.connect("aio.db") db.execute( """CREATE TABLE IF NOT EXISTS snapshot ( day TEXT, keyword TEXT, gl TEXT, hl TEXT, device TEXT, aio INTEGER, cited INTEGER, cite_pos INTEGER, rank INTEGER, sources TEXT, fetched_at TEXT, PRIMARY KEY (day, keyword, gl, hl, device))""" ) today = date.today().isoformat() used, failed = 0, [] for i in range(0, len(KEYWORDS), 20): body = run_batch(KEYWORDS[i:i + 20]) used += body["credits"]["used"] for row in body["results"]: if not row["success"]: # billed as part of the batch; re-run it if retryable if row.get("retryable"): failed.append(row["keyword"]) continue aio = row.get("aioverview") sources = aio["sources"] if aio else [] cite = [s["position"] for s in sources if mine(s.get("domain"))] rank = [o["position"] for o in row["organic"] if mine(o.get("domain"))] db.execute( "INSERT OR REPLACE INTO snapshot VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", ( today, row["keyword"], MARKET["gl"], MARKET["hl"], MARKET["device"], aio is not None, bool(cite), cite[0] if cite else None, rank[0] if rank else None, json.dumps([s["domain"] for s in sources if s.get("domain")]), row["fetchedAt"], ), ) db.commit() print(f"{len(KEYWORDS)} keywords, {used} credits, retry these:", failed)

Step 3: Schedule it

Run the tracker once a day at a fixed hour; Listing 5 is the crontab line. The fixed hour matters more than which hour you pick: the AI Overview is generated and changes between fetches, and a steady schedule keeps time of day out of the comparison. A repeat request for the same keyword and market within a short window can be answered from cache and is marked cached: true. That rarely matters for a daily run; send "cache": false in the batch body when you need every page fetched fresh.

Step 4: Report presence, citation rate and rank together

Listing 4 reads the latest snapshot and prints:

• AI Overview presence rate: keywords with an AI Overview divided by keywords checked. Track it per market and device; it is the size of the surface you are competing for.

• Citation rate: keywords where your domain is cited divided by keywords that showed an AI Overview.

• Rank and citation together: every keyword with an AI Overview falls into one of four groups: cited and on page one, cited but not on page one, on page one but not cited, or neither. The third group is the usual work list, because Google ranks the page and still cites someone else.

• Who is cited instead: the domains cited most often across your keywords.

• What changed since the last run: keywords that gained or lost an AI Overview or your citation, with today's organic rank beside them.

Because the text is generated, one day's citation can be noise. Call a keyword won or lost when the change holds for several runs, and report citation rates over a week rather than a single day's yes or no.

python
# Listing 4: report.py - presence, citation rate, rank vs citation, and what changed import json import sqlite3 from collections import Counter db = sqlite3.connect("aio.db") days = [d for (d,) in db.execute("SELECT DISTINCT day FROM snapshot ORDER BY day DESC LIMIT 2")] latest = days[0] rows = db.execute( "SELECT keyword, aio, cited, cite_pos, rank, sources FROM snapshot WHERE day = ?", (latest,) ).fetchall() shown = [r for r in rows if r[1]] cited = [r for r in shown if r[2]] print(f"{latest}: AI Overview on {len(shown)} of {len(rows)} keywords ({len(shown) / max(len(rows), 1):.0%})") print(f"Cited in {len(cited)} of those ({len(cited) / max(len(shown), 1):.0%})") # Rank and citation from the same page: the four groups groups = Counter() for keyword, aio, is_cited, cite_pos, rank, sources in shown: on_page_one = rank is not None and rank <= 10 groups[("cited" if is_cited else "not cited", "page one" if on_page_one else "not on page one")] += 1 for (c, p), n in sorted(groups.items()): print(f" {c:<10} {p:<16} {n}") # Who is cited instead of you domains = Counter(d for r in shown for d in set(json.loads(r[5]))) print("Most-cited domains:", domains.most_common(10)) # What changed since the previous run if len(days) == 2: before = { k: (a, c) for k, a, c in db.execute("SELECT keyword, aio, cited FROM snapshot WHERE day = ?", (days[1],)) } for keyword, aio, is_cited, cite_pos, rank, sources in rows: if keyword in before and before[keyword] != (aio, is_cited): was_aio, was_cited = before[keyword] print(f" {keyword}: AI Overview {was_aio}->{aio}, cited {was_cited}->{is_cited}, rank {rank}")
bash
# Listing 5: crontab - snapshot every day at 06:30, then append the report 30 6 * * * cd /opt/aio-tracker && python3 track.py && python3 report.py >> report.txt

What AI Overview tracking costs

A keyword check is 3 credits per results page whether or not Google shows an AI Overview, since the page is fetched either way, and the AI Overview carries no surcharge. At $0.30 per 1,000 credits, the Scale pack rate:

KeywordsMarketsScheduleChecks per 30 daysCreditsCost at $0.30 per 1,000
1001Daily3,0009,000$2.70
5001Daily15,00045,000$13.50
5003WeeklyAbout 6,500About 19,500$5.85
1,0003Daily90,000270,000$81.00

The 3,000 free credits cover 1,000 checks, which is 100 keywords daily for ten days. Packs are one-time purchases from $3.99 with no subscription, and the rate falls to $0.20 per 1,000 credits on the Enterprise pack, which makes the last row $54.00 (see pricing). Fetching a second results page with pages=2 doubles the cost of that keyword; if you only need deeper rank positions, the Rank Tracking API prices them per 10 results of depth.

Honest limits

• No official source. This is the AI Overview as it appears on the rendered results page, parsed. When Google changes the page, parsing can lag until it is updated.

• Not every keyword has one. When Google shows no AI Overview for the keyword, market and device, aioverview is null and the check is still a billed results page.

• Non-deterministic text. Two fetches minutes apart can differ in wording and in the sources they cite. Measure over repeated checks.

• Page one only. The AI Overview, People Also Ask and related searches come from the first results page; extra pages add organic results only.

• No AI Mode answers. aiModeAvailable says Google offered AI Mode for the query; Searlo does not return AI Mode conversations.

• Seconds per check. Run the tracker as a scheduled job, not inside a user request.

• Failed batch rows are billed. Re-run the retryable ones.

Beyond Google

AI Overviews are one AI surface. ChatGPT, Perplexity, Microsoft Copilot and Gemini answer the same questions with their own citations: How to track your brand in ChatGPT, Perplexity, Copilot and Gemini builds the same kind of tracker on the AI visibility API, and Best AI visibility tools in 2026 compares the dashboards if you would rather not build one. Every parameter is in the docs and the API reference.

Frequently Asked Questions

Is there an API for Google AI Overviews?

Google publishes no official one. Searlo's GET /serp with aioverview=true returns the AI Overview from the rendered results page (its text, markdown and cited sources with their domains) together with the organic results, ads, People Also Ask and related searches, for 3 credits per results page. The Google AI Overview API page lists every field.

How do I track whether my site is cited in AI Overviews?

Fetch each keyword with aioverview=true for every market you care about and match your domain, subdomains included, against aioverview.sources[].domain. Store the result with the date, the source position and your organic position from the same response, and report a citation rate over several days rather than a single check, because the AI Overview is generated and changes between fetches.

How often should I check AI Overviews?

Daily for the keywords you are actively working on, weekly for the long tail. Run at a fixed time so the schedule does not add variance, and act on changes that hold over several runs rather than on one day's result.

How much does AI Overview tracking cost?

3 credits per keyword per results page, with the AI Overview included: $0.90 per 1,000 checks at $0.30 per 1,000 credits, or $0.60 on the Enterprise pack. 100 keywords checked daily is 9,000 credits, or $2.70, a month. New accounts get 3,000 free credits, which is 1,000 checks.

Does ranking first on Google mean I will be cited in the AI Overview?

No. In an Ahrefs analysis of 863,000 keyword results pages published on March 2, 2026, 38% of the pages cited in AI Overviews ranked in the top 10 for the keyword and 31.0% ranked outside the top 100. That gap is why a tracker should store organic rank and citation side by side.

Can I track AI Overviews for a specific country, city or device?

Yes. gl and hl set the country and language, location (a Google geotargets name) or uule sets a city, and device takes desktop, mobile or tablet. Store them with every row, because AI Overview presence differs between them.

What happens when a keyword has no AI Overview?

aioverview comes back null and the rest of the page is returned as usual. The check is billed as a normal results page, because the page was fetched, and it still records your organic rank.

Ready to try Searlo?

Get 3,000 free credits. No credit card required.

Get Free API Key