AnswerLine Start free

, E-Commerce · ChatGPT

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

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:

{
  "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:

A monitoring design

  1. 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.
  2. Fix the markets. Pick the countries you sell in from the 213 ChatGPT answers from.
  3. Run on a schedule. Weekly is enough for trend lines; daily for launches or promotions. Use async batches with webhooks rather than synchronous calls.
  4. Store products and offers as rows. One row per product per run, one row per offer per product.
  5. Report on four questions: Are we recommended? At what rank? At what price and from which merchants? Which pages back the rating?
  6. Follow the citations. rating_grouped_citation.supporting_websites[] and the answer’s sources[] 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

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

  1. Expecting empty arrays. Without include.shopping, the fields are absent. With it, a non-shopping answer may still have no products.
  2. Matching on titles alone. Product titles vary by merchant; use id when present, then normalized URL, then title.
  3. Treating prices as numbers. They are display strings. Parse per market and keep the raw value.
  4. Reading only shoppingCards. It comes back empty on the ChatGPT answers served today; read products from inlineProducts and entities (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.

Try it on your own prompts

100 free requests on any engine, no card. One POST returns the answer, sources and citations as JSON.

Keep reading