ChatGPT shopping cards: track the products it recommends
When a ChatGPT answer has shopping intent, it can show product cards next to the text: product names, prices, merchants, ratings and links. For a brand or a retailer this is a product shelf, and the way to know where you stand is to read it repeatedly, per market.
Update, 2026-10-19: the signed-out ChatGPT answers served today show no merchant cards (last seen around 2026-09-24), so shoppingCards comes back empty. The products from ChatGPT’s shopping search come back in inlineProducts, with price, link and merchant offers. result.entities (type: "product", free, no include.shopping needed) lists the products the answer names. The card fields below apply when cards return; the metrics apply to inline products today.
Definitions
- Shopping card: a product carousel in a ChatGPT answer, returned as an item of
result.shoppingCards[]with categorytags[]and aproducts[]list. - Inline product: a product referenced inside the answer text, an item of
result.inlineProducts[]. Today it carries the products from ChatGPT’s shopping search. - Offer: one merchant’s listing for a product, with its own price, availability and URL.
- Product share of shelf: the fraction of product slots across a prompt set that belong to your brand.
Request shopping data
Product data is opt-in:
curl -X POST https://api.answerline.dev/v1/monitor/chatgpt \
-H "Authorization: Bearer $ANSWERLINE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "best wireless earbuds under $150 for running",
"country": "US",
"include": { "shopping": true }
}'
Without include.shopping: true, shoppingCards and inlineProducts are omitted from result entirely, not returned empty. The other request fields that matter for commerce:
country: required. ChatGPT answers from 213 countries; run each market you sell in as its own series, so differences between markets stay visible.state: not taken on ChatGPT. A request that sends it is refused with a 400 and not billed.include.searchQueries: addsresult.searchQueriesat no cost. The field exists, but it comes back empty on the ChatGPT answers served today. See query fan-out.include.ads: addsresult.ads[], the sponsored placements, at no cost. The field exists, but it comes back empty on the ChatGPT answers served today. See tracking ads in ChatGPT answers.
shoppingCards: the carousel
{
"shoppingCards": [
{
"tags": ["lightweight running earbuds", "budget sport earbuds"],
"products": [
{
"title": "Example Sport Buds 2",
"position": 1,
"url": "https://store.example.com/sport-buds-2?utm_source=chatgpt.com",
"price": "$129.00",
"featured_tag": "lightweight running earbuds",
"image_urls": ["https://images.example.com/buds.jpg"],
"rating": 4.6,
"num_reviews": 5120,
"id": "3250714974047560249",
"offers": [
{
"merchant_name": "Example Store + others",
"product_name": "Example Sport Buds 2 Wireless",
"url": "https://store.example.com/sport-buds-2?utm_source=chatgpt.com",
"price": "$129.00",
"details": "In stock online, free delivery",
"available": true,
"checkoutable": false,
"price_details": { "base": "$129.00", "total": "$129.00" },
"tag": { "text": "Best price" }
}
],
"rating_grouped_citation": {
"title": "Example Sport Buds 2",
"url": "https://store.example.com/sport-buds-2",
"supporting_websites": [{ "title": "Review", "url": "https://reviews.example.org/buds" }]
}
}
]
}
]
}
What each product field tells you:
| Field | Meaning | Use |
|---|---|---|
position |
1-indexed rank across all products in all cards, flat | Slot rank for share-of-shelf |
title |
Product name as shown | Match to your catalog |
id |
Product identifier | Stable key across runs when present |
price |
Current price as a display string | Price monitoring |
offers[0].merchant_name |
The store line under the product, such as "adidas + others" |
Who ChatGPT attributes the product to |
featured_tag |
The category or style tag for the product | Which angle it was recommended for |
rating, num_reviews |
Rating on a 0-5 scale and review count | Social proof shown next to you and competitors |
offers[] |
Per-merchant listings: merchant_name, product_name, url, price, details, available, checkoutable, price_details.base/total, tag.text |
Retailer coverage, stock, price consistency |
rating_grouped_citation |
title, url and supporting_websites[] behind the rating |
Which pages feed the rating |
image_urls[] |
Product images | Visual QA |
Two practical notes. position does not reset per card, so the first product of the second card continues the numbering. And product and offer URLs carry ChatGPT attribution parameters (utm_source=chatgpt.com), so strip query parameters you don’t need before matching URLs to your catalog.
inlineProducts: products in the answer
inlineProducts[] carries the products from ChatGPT’s shopping search on the answers served today, with price, link and merchant offers. The contract defines three parts per item:
product: a product record withtitle, 1-indexedposition,url,description,price,featured_tag,rating,num_reviews,image_urls,id,variants,offers[]andrating_grouped_citation, plus lookup metadata. Its offers can carry more detail than card offers, such asoriginal_price,merchant_subtitle,is_digitaland aprice_detailsbreakdown withdisplay_price,base,shipping,taxandtotal.render_as:inline,heroorblock, how the product is displayed.details:rationale.rationale, the generated reason for recommending the product, withrationale.citations, andreviewswith asummaryand areviews[]list ofsource,theme,summary,sentiment(positive,neutral,negative),rating,num_reviewsandcite_url.
A monitoring design
- Build a commerce prompt set. 20-50 buying-intent prompts per product line, in the forms buyers use: “best X for Y”, “best X under $Y”, “X vs Y”, “is X worth it”. Prompt set design covers sourcing and phrasing.
- Fix the markets. Pick the countries you sell in from the 213 ChatGPT answers from.
- Run on a schedule. Weekly is enough for trend lines; daily for launches or promotions. Use async batches with webhooks rather than synchronous calls.
- Store products and offers as rows. One row per product per run, one row per offer per product.
- Report on four questions: Are we recommended? At what rank? At what price and from which merchants? Which pages back the rating?
- Follow the citations.
rating_grouped_citation.supporting_websites[]and the answer’ssources[]show which pages the engine relied on. Those pages are where content and PR work can influence the answer; how AI engines choose citations explains the source side.
Parsing code
import re
import requests
API = "https://api.answerline.dev"
HEADERS = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}
def parse_price(raw: str | None) -> tuple[float | None, str | None]:
"""'$1,299.00' -> (1299.0, '$'). Returns (None, None) when no number is found."""
if not raw:
return None, None
m = re.search(r"([^\d\s.,]*)\s*([\d.,]+)", raw)
if not m:
return None, None
amount = m.group(2).replace(",", "") # assumes US formatting; adapt per market
try:
return float(amount), (m.group(1) or None)
except ValueError:
return None, m.group(1) or None
def product_row(prompt_id: str, block: str, p: dict, tags: list[str]) -> dict:
amount, currency = parse_price(p.get("price"))
return {
"prompt_id": prompt_id, "block": block, "rank": p.get("position"),
"title": p.get("title"), "product_id": p.get("id"), "merchant": (p.get("offers") or [{}])[0].get("merchant_name"),
"price_raw": p.get("price"), "amount": amount, "currency": currency,
"rating": p.get("rating"), "num_reviews": p.get("num_reviews"),
"offers": len(p.get("offers", [])), "tags": tags,
}
def product_rows(prompt_id: str, result: dict) -> list[dict]:
rows = [product_row(prompt_id, "card", p, card.get("tags", []))
for card in result.get("shoppingCards", []) for p in card.get("products", [])]
rows += [product_row(prompt_id, "inline", item.get("product", {}), [])
for item in result.get("inlineProducts", [])]
return rows
r = requests.post(f"{API}/v1/monitor/chatgpt", headers=HEADERS, timeout=360, json={
"prompt": "best wireless earbuds under $150 for running",
"country": "US",
"include": {"shopping": True},
})
r.raise_for_status()
rows = product_rows("earbuds-001", r.json()["result"])
The price parser assumes US number formatting. For markets that write 1.299,00 €, swap the separators per country before converting.
Detecting changes run over run
Most commerce questions are about change: a new competitor on the card, a price that moved, a merchant that dropped out. With product rows stored per run, a diff between two runs of the same prompt and market is short:
def product_key(row: dict) -> str:
return row["product_id"] or (row["title"] or "").strip().lower()
def diff_runs(previous: list[dict], current: list[dict]) -> dict:
prev = {product_key(r): r for r in previous}
curr = {product_key(r): r for r in current}
return {
"entered": [curr[k]["title"] for k in curr.keys() - prev.keys()],
"left": [prev[k]["title"] for k in prev.keys() - curr.keys()],
"rank_changes": [(curr[k]["title"], prev[k]["rank"], curr[k]["rank"])
for k in curr.keys() & prev.keys() if curr[k]["rank"] != prev[k]["rank"]],
"price_changes": [(curr[k]["title"], prev[k]["price_raw"], curr[k]["price_raw"])
for k in curr.keys() & prev.keys() if curr[k]["price_raw"] != prev[k]["price_raw"]],
}
One caution before alerting on the output: a single run can differ from the previous one without any lasting change, so alert when a difference persists across two or more consecutive runs, or when it shows up across many prompts at once.
A weekly digest from this diff covers most commerce questions: products that entered and left the answers for each category, rank moves for your own products, and price differences against your list prices.
Metrics that hold up
- Recommendation rate: share of prompts where at least one of your products appears in the shopping products.
- Share of shelf: your product slots divided by all product slots across the set.
- Average best rank: mean of your lowest
positionper prompt where you appear. - Price consistency: share of your offers whose
pricematches your list price for that market. Mismatches are either a data problem on a merchant feed or a discount you didn’t know about. - Merchant coverage: which
merchant_namevalues carry your products, and whether your preferred retailers are among them.
Generated answers vary between runs, so report rates over the whole prompt set and over several runs, not single-prompt snapshots. AI answer volatility covers how many runs make a trend.
Scale and cost
Run the set as async tasks in batches of up to 500:
[
{ "taskType": "CHATGPT", "idempotencyKey": "shop-2026-w38-earbuds-001-US",
"payload": { "prompt": "best wireless earbuds under $150 for running", "country": "US", "include": { "shopping": true } },
"webhook": { "url": "https://your-app.example/hooks/answerline" } }
]
Send it to POST /v1/async/task/batch. Prices at the Starter tier (/pricing has every tier):
| Request | Price |
|---|---|
ChatGPT task with include.shopping |
$0.0006 |
40 prompts × 1 market (US) × weekly (about 4.3 runs a month) × $0.0006 ≈ $0.10 a month.
Pitfalls
- Expecting empty arrays. Without
include.shopping, the fields are absent. With it, a non-shopping answer may still have no products. - Matching on titles alone. Product titles vary by merchant; use
idwhen present, then normalized URL, then title. - Treating prices as numbers. They are display strings. Parse per market and keep the raw value.
- Reading only
shoppingCards. It comes back empty on the ChatGPT answers served today; read products frominlineProductsandentities(type: "product").
The same monitoring shape works for AI Mode’s product blocks, described in the Google AI Mode API guide. See all ChatGPT fields on the ChatGPT engine page.
Questions
How do I get ChatGPT's product recommendations as data?
Send a ChatGPT request with include.shopping set to true, either POST /v1/monitor/chatgpt or a CHATGPT async task. The response adds result.shoppingCards[] and result.inlineProducts[]. On the signed-out ChatGPT answers served today (checked 2026-10-19) shoppingCards comes back empty; inlineProducts carries the products from ChatGPT's shopping search, with price, link and merchant offers. result.entities (type product) lists the products the answer names, free and without include.shopping.
How do I tell a real change in recommendations from run-to-run noise?
Diff two runs of the same prompt and market on a product id or a normalized title, and alert only when a difference holds across two or more consecutive runs or appears across many prompts at once.
How much does a ChatGPT shopping request cost?
At the Starter tier a ChatGPT request costs $0.0006, with or without include.shopping.
Why is shoppingCards missing from my response?
The field is omitted when include.shopping is false or unset. With the flag it comes back empty on the ChatGPT answers served today, which show no merchant cards; the products are in inlineProducts. Treat both fields as optional.
Are prices returned as numbers?
No. ChatGPT product and offer prices are strings as displayed, such as $57.00, so parse them into an amount and currency yourself and keep the original string.
Can I track product recommendations by US state?
No. ChatGPT answers at country level, from 213 countries; a request that sends state is refused with a 400 and not billed. Of the AI engines, only Gemini takes a US state.