How to pull citations from Gemini answers
Gemini cites when it grounds an answer by browsing, and doesn't when it doesn't. That makes pulling citations from this surface a slightly different job than on a search engine: you're not collecting a list that's always there, you're collecting a list that sometimes is — and the sometimes turns out to be the interesting measurement.
One POST to https://api.agentgeo.org/v1/fetches with "surfaces": ["gemini"] returns answers[].sources[] as structured JSON, every entry carrying title, url and position. This guide covers the manual copy, the one-call fetch in curl and Python, a small logger that turns runs into a grounding trend, the agent version over MCP, and the normalisation traps that quietly corrupt citation data.
To pull citations from a Gemini answer, read sources[] off the record: each entry is a page the engine used, with the order it used them in. By hand that's copy-paste from whatever links the app shows. Programmatically it's one POST — and the code has to handle two outcomes, not one, because a delivered answer with an empty sources[] is a normal result on this surface rather than an error to retry.
The manual way (and why it stops working)
It really is simple for one query. Ask Gemini your question, look for the links or grounding panel under the answer, and copy each one in order. Keep the order — first cited is not the same result as sixth. Paste them into a sheet next to the date and the query, and add a column for "no sources shown", because you'll need it more often than you expect.
Row two is where it falls apart.
- Half the answers have nothing to copy. Gemini frequently answers without browsing, so you'll open ten tabs to collect four source lists — and the six empties are data you'll forget to write down, which is the data this surface is actually about.
- Order dies in transit. Multi-select in a browser, paste into a sheet, and the positions scramble or the URLs vanish behind their titles.
- Hosts are the unit you'll actually want. Rolling
blog.globex.com/uptimeandglobex.com/pricingup into "Globex" is a parsing job, and by hand it's a parsing job you redo on every single row. - A query set multiplies everything. Twenty queries a week, each with a handful of links, is hundreds of transcriptions a month without one typo. Forever.
- No run history, no trend. The most interesting thing here is whether a query started being grounded at all — and that's a comparison across runs, which a sheet of today's links can't make.
Do it with one API call
The API returns the citation list already parsed, in the same shape it uses for all six engines — so the parser you write here also works against Perplexity, where the lists are far denser.
curl -X POST https://api.agentgeo.org/v1/fetches \
-H "Authorization: Bearer ag_live_..." \
-H "Content-Type: application/json" \
-d '{"query": "best uptime monitoring tool for an API-first startup", "surfaces": ["gemini"], "country": "US"}'Each entry in sources[] has a title, a url and a position. That's enough to print a per-query citation list, aggregate which hosts keep recurring, and — the part specific to this surface — count how many queries came back grounded at all:
import requests
from collections import Counter
from urllib.parse import urlparse
KEY = "ag_live_your_key_here"
QUERIES = [
"best uptime monitoring tool for an API-first startup",
"how to monitor API latency across regions",
"uptime monitoring with a status page and on-call alerts",
]
def host(url):
h = urlparse(url).netloc.lower()
return h[4:] if h.startswith("www.") else h
cited = Counter() # how often each host appears anywhere in a source list
top_slot = Counter() # how often each host is cited at position 1
grounded, ungrounded = [], []
for query in QUERIES:
resp = requests.post(
"https://api.agentgeo.org/v1/fetches",
json={"query": query, "surfaces": ["gemini"], "country": "US"},
headers={"Authorization": f"Bearer {KEY}"},
timeout=200, # the API waits up to 180s for live surfaces
)
resp.raise_for_status()
for answer in resp.json()["answers"]:
if answer.get("status") == "failed":
snap = (answer.get("providerFields") or {}).get("snapshot_id")
print(f"{query!r}: failed, 0 credits - snapshot_id {snap}")
continue
sources = answer.get("sources") or []
# Delivered with no sources is a valid record, not an error.
if not sources:
ungrounded.append(query)
print(f"\n{query}\n ungrounded - answered without citing a page")
continue
grounded.append(query)
print(f"\n{query}")
for src in sorted(sources, key=lambda s: s["position"]):
h = host(src["url"])
cited[h] += 1
if src["position"] == 1:
top_slot[h] += 1
print(f" {src['position']:>2}. {h} - {src['title']}")
total = len(grounded) + len(ungrounded)
rate = len(grounded) / total if total else 0
print(f"\ngrounding rate: {len(grounded)}/{total} ({rate:.0%})")
print("most-cited hosts")
for h, n in cited.most_common(10):
print(f" {n:>2}x {h} (position 1: {top_slot[h]})")One run of that is a snapshot: today's sources, the hosts that recur, and the share of your queries Gemini bothered to browse for. The reason to fetch citations programmatically is that you can keep them — append every run to a JSONL file and the grounding question becomes answerable over time:
# grounding_log.py - call log_run(query, answer) inside the fetch loop above
import json
import pathlib
from datetime import datetime, timezone
LOG = pathlib.Path("gemini-citations.jsonl")
def log_run(query, answer):
# One line per run: the query, when it ran, whether it grounded, and what
# it cited. The empty runs are rows too - they are the measurement.
sources = answer.get("sources") or []
row = {
"fetchedAt": answer.get("fetchedAt")
or datetime.now(timezone.utc).isoformat(),
"query": query,
"surfaceKey": answer["surfaceKey"],
"grounded": bool(sources),
"sources": [
{"position": s["position"], "url": s["url"], "title": s["title"]}
for s in sources
],
}
with LOG.open("a", encoding="utf-8") as f:
f.write(json.dumps(row, ensure_ascii=False) + "\n")
def grounding_rate(query):
# What share of this query's stored runs came back with any sources?
runs = []
for line in LOG.read_text(encoding="utf-8").splitlines():
if not line:
continue
row = json.loads(line)
if row["query"] == query:
runs.append(row)
if not runs:
return None
return sum(1 for r in runs if r["grounded"]) / len(runs)Call log_run(query, answer) inside the fetch loop and grounding_rate(query) gives you the number this surface is really about: how often Gemini goes and looks something up for that question. Each delivered record costs one credit whether or not it cited anything, and failed records cost nothing.
The measurement most people skip here is the simplest one: how often Gemini grounded at all. A chatbot only cites when it browses, so an empty sources[] is a statement about the topic, not about you — the model judged the question answerable from what it already holds. Log a boolean per run and the grounding rate becomes a leading indicator: a query that starts coming back with sources is a query Google has decided needs current evidence, and that is precisely the moment citation work on that topic starts paying. Tracking only the hosts you found means missing the change in whether there were any hosts to find.
Once you have a few weeks of runs, the pattern per query tells you what kind of work is even available:
| What you see across runs | What it means | What to do |
|---|---|---|
| Never any sources | Gemini treats the topic as settled background knowledge. | Citations aren't the lever. Work on how broadly and consistently you're described across the web. |
| Sources on some runs | The topic sits on the boundary — the model browses when it isn't confident. | Worth logging weekly. This is where citation opportunity opens and closes. |
| Sources on every run | Google has decided this question needs current evidence. | Treat it like a search result: the cited pages are specific, and specific is beatable. |
| Grounding rate rising over weeks | The topic is becoming contested, fast-moving or newly commercial. | Publish now — the citation slots are being handed out while you read this. |
| Grounded, but the same hosts every time | A small trusted set is being reused. | Getting in usually means being referenced by those hosts rather than displacing them. |
Let your agent do it
Citation work is where an agent earns its keep: it can fetch the sources, then go read the page that outranked you and draft the answer to it. Connect the MCP server once and the pull becomes an instruction:
claude mcp add agentgeo -- npx -y agentgeo-mcp --key ag_live_...agentgeo-mcp runs over stdio, has zero npm dependencies and needs Node.js 18+. Its single tool, fetch_raw_answers, returns each record verbatim — sources[] included, positions intact. country shapes the market; note that language isn't forwarded for Gemini, because the chatbot datasets behind this surface reject the field, so a locale question here is a country question. Then ask:
Pull Gemini's answers for our five monitoring queries. For each one tell me whether it cited any sources, and list the hosts with their positions. Then give me the share of queries that were grounded at all, and flag any that weren't grounded last month but are now.
The records come back unchanged — no scoring, no ranking, no conclusions. Everything downstream, from the diff to the content brief, happens in your agent with your context, and can be committed alongside the site it's about. Prefer to script it? The Python and curl paths hit the identical endpoint.
Common problems
Citation data corrupts quietly. On this surface, these five are where it happens:
- Empty is not failed. A record with
status: "delivered"andsources: []is correct, complete and costs a credit. Branch onstatusfirst and onlen(sources)second, and log the empty runs — dropping them silently is how a grounding rate turns into 100%. - Normalise URLs before aggregating. Tracking parameters, trailing slashes and redirect wrappers make one page look like two. Lowercase the host, strip a leading
www., drop the query string — then count. - Positions are per-answer, not global. Position 1 on a narrow long-tail query is not the same prize as position 1 on your head term. Always store the query next to the position, or the aggregate means nothing.
- You can't make Gemini browse. The
web_searchtoggle is honoured only bychatgpt; on Gemini it's ignored. Grounding is something you observe rather than something you request, which is exactly why the grounding rate is worth logging instead of engineering around. - Slow scrapes come back
failed. Requests wait up to 180 seconds, and a scrape that exceeds that budget returnsstatus: "failed"at zero credits with aproviderFields.snapshot_id. A follow-up call with that id and the same single surface redeems the finished answer instead of paying to re-scrape it.
Summary
For a single answer, copy whatever links Gemini shows you — it's right there and it costs nothing. For a query set you'll revisit, fetch it: sources[] arrives with titles, URLs and positions already separated, aggregation is a Counter, and appending every run to a JSONL file gives you the number this surface is actually about. Not just which hosts Gemini cited, but how often it decided to go and cite anyone at all.
Pull your first structured citation list from Gemini. Start free — no credit card and store run one today.
Start for freeFrequently asked questions
- How do I get the sources Gemini used for an answer?
- When Gemini grounds an answer it shows the pages it drew on, and you can copy them from the app. To get them as data, POST your query to
https://api.agentgeo.org/v1/fetcheswith"surfaces": ["gemini"]and readanswers[].sources[]— every entry carries atitle, aurland aposition, so nothing has to be parsed out of prose or HTML. - Why does Gemini sometimes show no sources?
- Because it didn't browse. Gemini is a chatbot surface, so it only cites when it grounds an answer in pages it fetched; otherwise it answers from what the model already holds. A record with
status: "delivered"and an emptysources[]is a real, complete result — treat it as a fact about the topic rather than a failed fetch. - Can I pull Gemini citations programmatically?
- Yes. AgentGEO runs a managed-scraper engine and returns each answer's citation list as structured JSON, so an ordinary HTTP request gets you the sources with their order intact. There's no headless browser to run, no proxies to rotate, and no selectors that break when the frontend changes.
- How do I track Gemini citations over time?
- Store every run, including the ones that cited nothing. Append each fetch to a JSONL file with the query, the timestamp, a
groundedboolean and the full source list. That gives you two trends: which hosts are being cited, and what share of your queries Gemini grounds at all — the second is often the earlier signal. - Does Gemini cite as often as Perplexity?
- No, and it isn't close. Perplexity is search-grounded, so it retrieves before it writes and carries a source list on essentially every answer. Gemini cites only when it browses. The record shape is identical across all six surfaces, so the same parser works everywhere — but don't compare citation counts between a chatbot and a search-grounded engine without saying which is which.
Keep reading