Google AI Mode API
Google's conversational AI Mode answer with citation pills, places, shopping cards and videos.
Price
$2.06 per 1,000 requests on every plan, deducted from your prepaid USD balance.
Endpoint
POST /v1/monitor/aimode
Countries
213 countries, down to the city.
Request
curl -X POST "https://api.answerline.dev/v1/monitor/aimode" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt":"Family-friendly hotels in Lisbon","country":"PT","hl":"en"}'
Request options
| Field | Type | Description |
|---|---|---|
prompt * |
string |
Prompt submitted to Google AI Mode, up to 2,000 characters. A longer prompt is refused with a 400 that states its length. |
country |
string |
Code of the country or region used to localize the response |
gl |
string |
Google's own name for the result-geography parameter, usable in place of `country`. Takes ISO 3166-1 alpha-2 codes, as `country` does, in upper or lower case (`us` or `US`), and the country has to be one this endpoint supports (list them with `GET /v1/countries`; Google News has no `model` value there, and its edition countries are listed at https://answerline.dev/docs/coverage#countries). Send either `country` or `gl`; if both are sent with differing values, the request fails with `400`. |
hl |
string |
Interface language, in the format of Google's `hl` parameter (for example `en`, `de`, `pt-BR`). Case-insensitive free text; Google defines which codes exist. Takes precedence over the language Answerline infers from the country, which lets interface language and result geography differ. |
location |
string |
Location name in Google's canonical form, used for geo-targeted results, written comma-separated as 'City,Region,Country' (https://developers.google.com/google-ads/api/reference/data/geotargets lists every supported location, ~100,000 in all). Combine it with 'country' to target a specific city. Cannot be combined with 'uule'; send only one of the two. |
uule |
string |
A ready-made Google UULE string for exact geo-targeting, in the location-name form (starting w+CAIQICI); the coordinate form (a+) is refused with a 400. Supply it when you already have an encoded UULE rather than a location name. Cannot be sent together with 'location' — pass only one of the two. |
device |
"desktop" | "mobile" |
Type of device the results are fetched for |
include |
object |
Flags that add extra output formats to the response (optional) |
include.html |
boolean |
A copy of the AI Mode page as it was when the answer was captured, stored on our file host and linked in `html`, to check the answer against. No extra cost. |
include.markdown |
boolean |
Add a markdown version of the response to the result |
include.expandProducts |
boolean |
Not available yet: a request that sets it is refused with 400 before anything runs or is charged. It is meant to look up merchant offers for the response's product clusters and return them in 'result.productResults'. |
include.googleGoto |
boolean |
Has had no effect since 8th September 2026. Each redirect link is now always returned as `redirectLink` next to its target, so you get both without setting this flag. Requests that still include it are handled as though it were omitted. |
include.rawWire |
boolean |
Return the engine's own response as received in result.rawWire: the answer stream, the rendered results pages or the RSS feed, without Gemini's detected location or ChatGPT's session tokens; null where it was not captured. Free. See https://answerline.dev/docs/output-formats#raw-wire |
include.screenshot |
boolean |
Return a full-page PNG screenshot of the engine's page as it showed the finished answer, in result.screenshot: a link, or one link per results page on Google Search. On ChatGPT, Gemini, AI Mode, Google Search, Perplexity and Grok; other engines refuse it with a 400. Absent when the answer was read without a rendered page or its page was not available. Free. See https://answerline.dev/docs/output-formats#screenshots |
answerDepth |
"any" | "full" | "quick" |
How deep an answer the engine writes, where it offers a choice. any (the default): a full answer when the engine can give one now, else its short one. full: a full answer or a retryable error, never charged. quick: the short one, the fastest. Perplexity offers the choice; every other engine has one depth and ignores the field. provenance.answerDepth says what was served. Same price for every value. See https://answerline.dev/docs/engine-features/#answer-depth |
What you get back
Field in result | Type | Description |
|---|---|---|
text | string | Text of the AI Mode answer |
markdown | string | The AI Mode answer rendered as Markdown (present when include.markdown is true) |
sources | object[] | Sources the response cites |
citationPills | object[] | Citation pills that appear inline in the AI Mode answer. Every element pairs one pill with one source: a pill backed by N sources (Google's `[Label +N]` chips) yields N elements with the same `citationPillId`, each holding that source's own `label`, `url` and `domain`. Grouping by `citationPillId` rebuilds the pills. Not present in `result` if the answer has no pills. |
places | object[] | Place cards embedded inline in the response |
shoppingCards | object[] | Shopping-related product details parsed from the response |
inlineProducts | object[] | Product cards that appear inside the AI answer text |
videos | object[] | Video cards shown inline within the AI response (YouTube for now) |
map | object[] | Locations pulled from the response, enriched with GPS coordinates |
ads | object | Section of sponsored ads parsed from the response |
AI Mode answers versus Google’s developer APIs
Google AI Mode is Google Search’s conversational answer surface. This endpoint returns that answer for a prompt, market, city and language, with citation pills, places, map pins, shopping cards, videos and, when Google shows sponsored products, ads as structured fields. inlineProducts is in the schema and comes back empty: Google shows an inline product mention without a name or price.
Google’s long-standing developer route to search results is the Custom Search JSON API. Its overview (checked 2026-09-17, developers.google.com) says it returns web and image results from a Programmable Search Engine, offers 100 free queries per day and $5 per 1,000 beyond that up to 10,000 per day, is closed to new customers, and gives existing customers until January 1, 2027 to transition. Its cse.list reference (checked 2026-09-17, developers.google.com) documents gl, hl, cr and lr parameters and does not mention AI Overviews, ads, local packs or knowledge panels.
In AI Mode users read a composed response instead of scanning ten links, so whether your brand is named, and in which sentence, is the visibility metric. Citations appear as [Label +N] chips that bundle several sources; citationPills records every source behind every chip, so you can measure both “cited at all” and “cited first in a chip”. Products (shoppingCards), sponsored products (ads), local businesses (places, map) and videos sit inside the answer and vary by location and device.
An annotated response
The excerpt follows the result schema for POST /v1/monitor/aimode. Values are illustrative.
{
"success": true,
"result": {
"text": "For families, look for hotels near the riverfront with interconnecting rooms...",
"sources": [
{
"position": 1,
"url": "https://www.example-travel.com/lisbon/family-hotels",
"label": "Best family hotels in Lisbon",
"description": "Rooms for four, pools and quiet streets..."
}
],
// One entry per (chip, source). citationPillId is 1-based; position points
// into result.sources. redirectLink appears only when Google served a /goto link.
"citationPills": [
{
"label": "Best family hotels in Lisbon",
"citationPillId": 1,
"url": "https://www.example-travel.com/lisbon/family-hotels",
"domain": "example-travel.com",
"position": 1
}
],
"places": [
{
"title": "Example Riverside Hotel",
"link": "https://www.google.com/viewer/place?example",
"placeId": "ChIJexampleLisbon",
"index": 0,
"rating": 4.6,
"reviews": 2140,
"type": "Hotel",
"priceLevel": "€€€",
"address": "Rua Exemplo 10, Lisboa"
}
],
"map": [
{
"title": "Example Riverside Hotel",
"placeId": "ChIJexampleLisbon",
"gps_coordinates": { "latitude": 38.707, "longitude": -9.136 },
"rating": 4.6
}
],
// position is 0-indexed within the shopping carousel.
"shoppingCards": [],
// ads is an object: a section title plus the ads in it.
"ads": { "title": "Sponsored", "ads": [] },
"videos": [
{
"title": "Lisbon with kids: where to stay",
"link": "https://www.youtube.com/watch?v=example",
"platform": "YouTube",
"channel": "Example Family Travel",
"duration": "12:04"
}
]
}
}
Shape details to code for:
adsis an object withtitleandads[], not an array, andnullwhen the answer shows no sponsored products. Each ad hastitle,url,positionandstore.shoppingCards[].positionis 0-indexed;inlineProducts[].positionis 1-indexed. Prices are objects:price.valueis numeric, andprice.rawkeeps the displayed string on shopping cards.reviewsis a string on shopping cards and ads, but an integer onplacesandmap.redirectLinksits beside resolved links on sources, pills, snippet links, ads and videos. Resolve one yourself withPOST /v1/monitor/google/goto, which is free and runs at most 4 calls per account at once.include.googleGotois deprecated and ignored; redirect links are returned without it.
Synchronous calls
A synchronous call waits up to five minutes. It costs $0.00206 on every tier, the same as a task.
Python (httpx)
import os
import httpx
API = "https://api.answerline.dev"
with httpx.Client(base_url=API, headers={"Authorization": f"Bearer {os.environ['API_KEY']}"}, timeout=330) as client:
resp = client.post("/v1/monitor/aimode", json={
"prompt": "Family-friendly hotels in Lisbon",
"country": "PT",
"hl": "en",
})
if resp.status_code != 200:
raise RuntimeError(f"{resp.status_code} {resp.headers.get('X-Request-Id')} {resp.text}")
result = resp.json()["result"]
chips = {}
for pill in result.get("citationPills", []):
chips.setdefault(pill["citationPillId"], []).append(pill["domain"])
print("chips:", chips)
print("places:", [(p.get("title"), p.get("rating")) for p in result.get("places", [])])
print("credits charged:", resp.headers.get("X-Credits-Charged"))
TypeScript (fetch)
const API = "https://api.answerline.dev";
const res = await fetch(`${API}/v1/monitor/aimode`, {
method: "POST",
headers: { Authorization: `Bearer ${process.env.API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
prompt: "best standing desk for a small apartment",
gl: "us",
location: "Austin,Texas,United States",
include: { markdown: true },
}),
signal: AbortSignal.timeout(330_000),
});
if (!res.ok) throw new Error(`${res.status} ${res.headers.get("X-Request-Id")} ${await res.text()}`);
const { result } = await res.json();
for (const card of result.shoppingCards ?? []) {
console.log(card.position, card.title, card.price?.raw ?? card.price?.value, card.store);
}
for (const ad of result.ads?.ads ?? []) console.log("ad:", ad.position, ad.title, ad.store);
Batches: async tasks and a webhook
Tasks cost $0.00206 on every tier, wait in your queue until they start, and post to your webhook. POST /v1/async/task/batch takes 1 to 500 tasks, each admitted on its own and answered by index.
Python: a city grid
import datetime
import os
import requests
API = "https://api.answerline.dev"
HEADERS = {"Authorization": f"Bearer {os.environ['API_KEY']}"}
day = datetime.date.today().isoformat()
cities = ["Austin,Texas,United States", "Denver,Colorado,United States", "Seattle,Washington,United States"]
prompts = ["emergency plumber near me", "best pizza for delivery"]
tasks = [
{
"taskType": "AIMODE",
"payload": {"prompt": prompt, "country": "US", "location": city},
"idempotencyKey": f"aim-{i}-{city.split(',')[0].lower()}-{day}",
"webhook": {"url": "https://your-app.example/hooks/answers"},
}
for i, prompt in enumerate(prompts)
for city in cities
]
resp = requests.post(f"{API}/v1/async/task/batch", headers=HEADERS, json=tasks, timeout=60)
resp.raise_for_status()
body = resp.json()
print(body["summary"], "reserved:", sum(r["credits"]["creditsToCharge"] for r in body["results"] if r["success"]))
TypeScript: receive deliveries and check your listing (Express)
Each delivery is the GET /v1/async/task/{taskId} body. Verify Webhook-Signature over the raw body before parsing.
import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";
const MY_PLACE_IDS = new Set(["ChIJexampleLisbon"]);
function verify(raw: Buffer, header: string | undefined, secret: string, toleranceSeconds = 300): boolean {
const pairs = (header ?? "").split(",").map((part) => part.split("="));
const stamps = pairs.filter(([k]) => k === "t").map(([, v]) => v);
const sigs = pairs.filter(([k]) => k === "v1").map(([, v]) => v);
if (stamps.length !== 1 || !/^\d+$/.test(stamps[0]) || sigs.length === 0) return false;
if (Math.abs(Date.now() / 1000 - Number(stamps[0])) > toleranceSeconds) return false;
const expected = createHmac("sha256", secret).update(`${stamps[0]}.`).update(raw).digest();
return sigs.some((sig) => {
const given = Buffer.from(sig, "hex");
return given.length === expected.length && timingSafeEqual(given, expected);
});
}
const app = express();
app.post("/hooks/answers", express.raw({ type: "application/json" }), (req, res) => {
if (!verify(req.body, req.get("webhook-signature"), process.env.WEBHOOK_SECRET!)) return res.sendStatus(400);
const delivery = JSON.parse(req.body.toString("utf8"));
if (delivery.test) return res.sendStatus(204);
const { task, response } = delivery;
if (task.status !== "COMPLETED") {
console.warn(task.idempotencyKey, "failed:", response.error.code);
return res.sendStatus(204);
}
const ranks = (response.result.places ?? []).filter((p: any) => MY_PLACE_IDS.has(p.placeId)).map((p: any) => p.index);
console.log(task.idempotencyKey, ranks.length ? "listed at" : "not listed", ranks);
res.sendStatus(204);
});
app.listen(3000);
Deduplicate on task.id; see webhooks.
Geo and language targeting
| Field | Values | Notes |
|---|---|---|
country or gl |
ISO 3166-1 alpha-2, either case for gl |
One is required; sending both with different values is a 400. GET /v1/countries?model=aimode lists codes. |
hl |
Google interface language, e.g. en, de, pt-BR |
Overrides the language derived from the country. |
location |
Google canonical name, City,Region,Country |
City-level precision; the names come from Google Ads geotargets. Not with uule. Without it, Google is localized to the requested country. |
uule |
Pre-encoded UULE string | Alternative to location. |
device |
desktop, mobile |
Default desktop. |
For a region or city below the country, use location (for example Texas,United States).
Include flags and what they cost
An AI Mode task is $2.06 per 1,000 on every tier.
| Flag | Effect | Price |
|---|---|---|
include.markdown |
Adds markdown |
0 |
include.html |
Adds html: a link to a copy of the AI Mode page (HTML snapshots) |
0 |
include.expandProducts |
Not available yet: a request that sets it is refused with a 400 | Not charged |
include.googleGoto |
Deprecated, ignored | 0 |
include.rawWire |
Adds rawWire: the AI Mode page as rendered, when it was captured (what it holds) |
0 |
include.screenshot |
Adds screenshot: a link to a full-page PNG of the AI Mode page once the answer finished (details) |
0 |
Common workloads
- AI visibility tracking in Google’s conversational search. See AI visibility tracking and the AI Mode API post.
- Local rank tracking: position of your business in
placesby city. See local rank tracking. - Rank tracking beyond blue links: pair AI Mode with Google Search results for the same keyword. See rank tracking.
- Competitor and shelf analysis: which stores and products appear in
shoppingCardsand sponsoredads. See competitor analysis.
Pitfalls
location expects Google’s canonical City,Region,Country names from the Google Ads geotargets list. Copy names from that list instead of typing them, and keep a vetted list per client.
hl overrides the language derived from the country. When comparing markets, set hl explicitly; see multilingual AI answers.
shoppingCards[].position starts at 0 and inlineProducts[].position at 1. Normalize before storing ranks.
A city grid grows fast: 2 prompts × 3 cities is 6 tasks per day before repeated samples, and answers vary enough between runs that you want several per combination (AI answer volatility). There is no concurrency cap and no request rate limit; tasks queue (about 100,000 tasks of a typical prompt at a time). See rate limits.
How failures are billed
Accepted requests reserve their maximum cost (credits.creditsToCharge) and are charged the actual cost on success (credits.creditsCharged, or X-Credits-Charged on a synchronous call), never more than reserved. A failed request is charged nothing and its reservation is released, including tasks that never start within 4 hours (QUEUE_WAIT_EXCEEDED). Synchronous calls may complete and be charged after a dropped connection, so resend only if the request never reached the server. See credits.
Related: getting AI Mode answers as JSON and monitoring Google AI Overviews.
Questions
How much does a Google AI Mode request cost?
A request is $2.06 per 1,000 on every tier.
Can I target a city in AI Mode?
Yes. Send country (or gl) plus location as a Google canonical location name such as Austin,Texas,United States, or a pre-encoded uule instead of location. location and uule are mutually exclusive.
What is redirectLink in AI Mode results?
When Google serves a link as a /goto redirect, the link field holds the resolved destination and redirectLink holds Google's redirect. POST /v1/monitor/google/goto resolves a redirect link on its own and is free.
Can I set the interface language separately from the country?
Yes. hl sets Google's interface language, for example en or pt-BR, and overrides the language derived from the country, so language and result geography can differ.
Is a failed AI Mode request charged?
No. A failed request is charged nothing and its reservation is released. A completed request is charged the base price.