Monitor Google Gemini answers with an API: fields and code
Monitoring Gemini means running a fixed prompt set against Google’s Gemini on a schedule, per market, and storing the answer, its sources and its inline citations as structured data you can compare over time. Below: the request, the response fields, costs, a scheduled batch and the mistakes that distort the numbers.
Terms
A source is an entry in result.sources, the list of pages Gemini attached to the answer. A citation pill is an inline chip in the answer text that points at one or more sources, returned as result.citationPills. A run is one execution of the whole prompt set across its markets.
Google’s help page for Gemini Apps notes that sources appear in or below a response when available, and that “not all responses include related links or sources” (View related sources in Gemini Apps, checked 2026-09-17). An answer without sources is a normal outcome, and your metrics need to allow for it.
The endpoint and request fields
POST https://api.answerline.dev/v1/monitor/gemini, with Authorization: Bearer <key> as described in authentication.
| Field | Required | Notes |
|---|---|---|
prompt |
yes | The prompt sent to Gemini (up to 2,000 characters). |
country |
yes | Country code; GET /v1/countries?model=gemini lists supported ones. Gemini answers from 213 countries. |
state |
no | Two-letter US state code, only with country: "US"; DC included. |
include.markdown |
no | Adds result.markdown. |
include.html |
no | Adds result.html, a link to a copy of the Gemini page as it was when the answer was captured. No extra cost. |
include.rawResponse |
no | Adds result.rawResponse, the answer as normalized stream events; include.rawWire returns the answer stream as Gemini sent it, when it was captured. |
The body accepts no other fields; unknown keys fail validation.
Step 1: send a prompt
curl
curl -X POST https://api.answerline.dev/v1/monitor/gemini \
-H "Authorization: Bearer $ANSWERLINE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "which email platform is best for newsletters?",
"country": "US",
"include": { "markdown": true }
}'
Python (requests)
import os
import requests
API = "https://api.answerline.dev"
HEADERS = {"Authorization": f"Bearer {os.environ['ANSWERLINE_API_KEY']}"}
resp = requests.post(
f"{API}/v1/monitor/gemini",
headers=HEADERS,
json={"prompt": "which email platform is best for newsletters?", "country": "US", "include": {"markdown": True}},
timeout=360,
)
resp.raise_for_status()
result = resp.json()["result"]
print(result.get("markdown") or result.get("text"))
for source in result.get("sources", []):
print(source["position"], source["url"], source.get("label"))
print("charged (USD):", resp.headers.get("X-Credits-Charged"))
TypeScript (@answerline/sdk)
import { Client } from "@answerline/sdk";
interface GeminiResult {
text?: string;
markdown?: string;
sources?: { position: number; label?: string; url: string; description?: string }[];
citationPills?: { citationPillId: number; position: number; url: string; domain: string; label: string; description?: string }[];
places?: { index: number; title: string; link: string; rating?: number; reviews?: number; type?: string; price?: string }[];
}
const client = new Client({ apiKey: process.env.ANSWERLINE_API_KEY! });
const { result } = await client.monitor.gemini<GeminiResult>({
prompt: "which email platform is best for newsletters?",
country: "US",
});
console.log((result.sources ?? []).map((s) => s.url));
The TypeScript SDK is on npm as @answerline/sdk; the Python examples call the HTTP API with requests.
Step 2: read the response fields
A successful call returns { "success": true, "result": { ... } }.
| Field | Type | Meaning |
|---|---|---|
text |
string | Gemini’s answer as plain text. |
markdown |
string | The answer in Markdown, when include.markdown is true. |
sources[] |
array | position, label, url, description for each source. |
citationPills[] |
array | One entry per pill-and-source pair: citationPillId, position, url, domain, label, optional description. Omitted when the answer has no pills. |
places[] |
array | Place cards for lodging and local answers. |
rawResponse[] |
array | The answer as normalized stream events, when include.rawResponse is true. result.rawWire (with include.rawWire) is the answer stream as Gemini sent it, when it was captured. |
Sources versus citation pills
sources is the list; citationPills tells you where in the answer those sources are used. When one chip cites several sources, the array holds one entry per source, all sharing the same 1-based citationPillId. Each entry’s position is the 1-based index of that source in result.sources, so you can join the two. label is always present but can be an empty string when Gemini showed no title; use domain or url for identity in that case.
Rebuilding the chips:
from collections import defaultdict
def chips(result: dict) -> dict[int, list[str]]:
grouped = defaultdict(list)
for pill in result.get("citationPills", []):
grouped[pill["citationPillId"]].append(pill["domain"])
return dict(grouped)
# e.g. {1: ["example.com"], 2: ["example.org", "example.net"]}
A source that is listed but never cited inline is less visible than one inside a chip, so track “in sources” and “in a chip” separately.
Place cards
For lodging and local prompts (“quiet hotels near Kings Cross”), places[] returns index, title, link (a Google URL for the place) and, when Gemini shows them, thumbnail, rating (0 to 5), reviews, type, description and price as Gemini displays it. For hospitality and local businesses this block is the recommendation list; measure presence and index, and combine with state targeting (local AI answers).
Step 3: compute visibility metrics
from urllib.parse import urlsplit
def on_domain(url: str, domain: str) -> bool:
host = (urlsplit(url).hostname or "").lower()
return host == domain or host.endswith("." + domain)
def gemini_metrics(result: dict, brand: str, domain: str) -> dict:
sources = result.get("sources", [])
pills = result.get("citationPills", [])
text = result.get("text", "")
return {
"has_sources": bool(sources),
"mentioned": brand.lower() in text.lower(),
"source_positions": [s["position"] for s in sources if on_domain(s["url"], domain)],
"in_chip": any(on_domain(p["url"], domain) for p in pills),
"place_rank": next((p["index"] for p in result.get("places", []) if brand.lower() in p["title"].lower()), None),
}
Aggregate per prompt, market and run:
- Sourced-answer rate: share of answers with
has_sources, which tells you how much of your prompt set can produce a citation on Gemini at all. - Citation rate: among sourced answers, share where your domain appears in
sources. - Chip rate: share where your domain is inside a citation pill.
- Mention rate: share of all answers naming the brand in
text, cited or not.
The difference between mention and citation is covered in mentions vs citations; for a full scoring model see AI share of voice.
Step 4: run it on a schedule
Scheduled monitoring should use async tasks: taskType: "GEMINI" with the same body as payload.
def gemini_tasks(prompts: dict[str, str], countries: list[str], day: str, hook: str) -> list[dict]:
# prompts maps a stable prompt id to its text
return [
{
"taskType": "GEMINI",
"payload": {"prompt": text, "country": country},
"idempotencyKey": f"gemini-{pid}-{country}-{day}",
"webhook": {"url": hook},
}
for pid, text in prompts.items()
for country in countries
]
tasks = gemini_tasks({"newsletter-platform": "which email platform is best for newsletters?",
"crm-small-team": "which CRM suits a five-person sales team?"},
["US", "GB"], "2026-09-17", "https://example.com/hooks/answers")
batch = requests.post(f"{API}/v1/async/task/batch", headers=HEADERS, json=tasks, timeout=360).json()
print(batch["summary"]) # {"total": 4, "succeeded": ..., "failed": ...}
The same with the SDK, for one country:
const prompts: Record<string, string> = {
"newsletter-platform": "which email platform is best for newsletters?",
"crm-small-team": "which CRM suits a five-person sales team?",
};
const tasks = Object.entries(prompts).map(([pid, prompt]) => ({
taskType: "GEMINI" as const,
payload: { prompt, country: "US" },
idempotencyKey: `gemini-${pid}-US-2026-09-17`,
webhook: { url: "https://example.com/hooks/answers" },
}));
const batch = await client.createBatch(tasks);
for (const item of batch.results) if (!item.success) console.warn(tasks[item.index].idempotencyKey, item.error.code);
Each finished task is POSTed to the webhook as the GET /v1/async/task/{id} JSON; the Gemini result is under response.result. Verify the signature first (verify webhook signatures), dedupe by task.id, and return a 2xx within 15 seconds. The full pipeline design is in webhook reliability.
What it costs
Per billing, at the Starter tier:
| Request | Price |
|---|---|
| Gemini request | $0.0006 |
include.markdown or include.rawResponse |
+0 |
Worked example: 100 prompts with 3 runs each is 100 x 3 x $0.0006 = $0.18. A new account’s 100 free requests cover 100 Gemini tasks, enough for 33 prompts at 3 runs each. Size larger programs with cost planning and compare tiers on pricing.
Sync or async
- Use synchronous calls when someone waits for the result, such as an in-app “check this prompt” action or a notebook session. The caller holds the connection until the answer is ready.
- Use async tasks for everything scheduled. They queue with no count limit (about 100,000 typical tasks per account), go in batches of up to 500, and accept
priority1 to 10 to order your own pending tasks.
More on the trade-off in sync calls, async tasks or webhooks.
Pitfalls
- Counting an empty
sourcesarray as a failure. It is a valid Gemini answer without citations. Keep it in the denominator of the mention rate and out of the citation rate. - Using
labelas the source identity. It can be empty. Key onurlordomain. - Mixing markets. Answers from different countries are different results, and so are answers from different US states. Keep country (and state) as a dimension in every metric from the start; see multilingual AI answers for language effects.
- One run per prompt. Answers vary between runs. Trend over several runs before concluding anything (answer volatility).
- Retrying a failed synchronous call in a loop. A 500 or 502 is final and uncharged; resubmit later or as a task with an idempotency key (API errors and retries).
Gemini next to the other engines
Gemini is one engine in a cross-engine program. Run the same prompt set on ChatGPT, then compare where your domain is cited on one and missing on the other. How engines pick citations in general is covered in how AI engines choose citations; reporting is covered on the AI visibility tracking page.
The request example and field summary live on the Gemini engine page; to make your first call, start with the quickstart.
Questions
Why is a Gemini source listed but never cited in the answer?
sources is the full list of what Gemini drew on; citationPills records where those sources are used inline, one entry per pill and source pair sharing a citationPillId, with position pointing back into sources. A source that never appears in a pill is less visible than one inside a chip, so track the two separately.
Why is result.sources empty for some Gemini answers?
Gemini does not attach sources to every answer, so an empty sources array is a valid result. Record it as an answer without citations rather than as an error.
Can a Gemini source label be empty?
Yes. label is always present but can be an empty string when Gemini showed no title, so identify a source by url or domain and keep label for display only.
What are Gemini place cards in the response?
result.places lists the place cards Gemini returns for lodging and local answers, with index, title, link and, when shown, thumbnail, rating, reviews, type, description and price.
Can I monitor Gemini in a specific US state?
Yes. Send state, for example CA (DC included), together with country US.