Tutorial2026-07-156 min read

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.

Tutorial

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/uptime and globex.com/pricing up 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 runsWhat it meansWhat to do
Never any sourcesGemini 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 runsThe 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 runGoogle 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 weeksThe 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 timeA 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" and sources: [] is correct, complete and costs a credit. Branch on status first and on len(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_search toggle is honoured only by chatgpt; 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 returns status: "failed" at zero credits with a providerFields.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 free

Frequently 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/fetches with "surfaces": ["gemini"] and read answers[].sources[] — every entry carries a title, a url and a position, 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 empty sources[] 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 grounded boolean 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