Copilot shopping results
Product cards and map results inside Copilot answers, as structured records. Coming soon.
Shopping cards and map results are coming. Once they ship, commerce and local answers will carry `shoppingCards` and `map` entries next to the text: the products, numeric prices, sellers and places the answer recommends. Today a Copilot result carries the answer text, markdown and its cited sources.
Request
curl -X POST "https://api.answerline.dev/v1/monitor/copilot" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt":"Cheapest way to send money abroad","country":"US"}'
What Copilot shopping and map results are
Copilot shopping results are the product cards Copilot shows inside an answer: product name, brand, seller, a structured price, rating, images and specifications. Map results are the businesses it pins for local questions, with address, coordinates, reviews and opening state. They are coming to this API as shoppingCards and map, next to the answer text and its sources. Today a Copilot result carries the answer text, markdown and cited sources, and neither field is returned yet.
For a buyer asking an assistant what to purchase or where to go, these modules are the recommendation. They show which retailers and brands hold the product slots for a category and in which positions, the spread of prices shown for a query and where a brand or seller sits in it, and whether a business appears for a local question, with which rating and open state.
Field reference
The fields below are the shape shoppingCards and map will take when they ship.
result.shoppingCards[]
| Field | Type | Meaning |
|---|---|---|
type |
string | Shopping card type. |
layout |
string | Layout style for the card. |
products |
array | Products in the card (below). |
result.shoppingCards[].products[]
| Field | Type | Meaning |
|---|---|---|
product.id |
string | Unique product ID. |
product.groupId |
string | Product group ID. |
product.brandGroupId |
string | Brand group ID, optional. |
position |
integer | 1-indexed rank across all products in all cards; not reset per card. |
offerId |
string | Unique offer identifier. |
url |
string | Product page URL. |
name |
string | Product name. |
description |
string | Product description. |
images[] |
array of {title, url} |
Product images. |
specifications[] |
array of {displayName, values[]} |
Specifications such as Color or Size, with available values. |
tags[] |
array of strings | Product tags. |
price.amount |
number | Price amount. |
price.currency |
string | Currency code, optional. |
price.currencySymbol |
string | Currency symbol. |
seller |
string | Seller name. |
sellerLogoUrl |
string | Seller logo URL. |
brandName |
string | Product brand name. |
rating.value |
number | Rating value. |
rating.count |
integer | Number of ratings. |
canTrackPrice |
boolean | Whether price tracking is available. |
result.map[]
| Field | Type | Meaning |
|---|---|---|
name |
string | Business name. |
position |
integer | Position in results, 1-indexed. |
placeId |
string | Google Place ID. |
location.address |
string | Full street address. |
location.latitude |
number | GPS latitude. |
location.longitude |
number | GPS longitude. |
phoneNumber |
string | Contact phone number. |
url |
string | Business website URL. |
reviews[] |
array of {count, rating, providerName, providerIconUrl, url} |
Review aggregations per provider. |
photos[] |
array of {url, altText, providerName, providerUrl} |
Business photos. |
openState |
string | Current open or closed status. |
category |
string | Business category. |
price |
any | Price level, if available. Can be any JSON value. |
layerLabel |
string | Map layer grouping label. |
Every field in both arrays is optional.
Annotated excerpt
Values are illustrative; field names and types follow the POST /v1/monitor/copilot result schema, as the response will look once shopping and map results ship.
{
"success": true,
"result": {
"text": "Here are well-reviewed trail running shoes under $150...",
"shoppingCards": [
{
"type": "product",
"layout": "carousel",
"products": [
{
"product": { "id": "p-1001", "groupId": "g-501" },
"position": 1, // flat rank across every card
"offerId": "o-88231",
"url": "https://shop.example-outdoor.com/trail-runner-4",
"name": "Trail Runner 4",
"brandName": "Example Stride",
"seller": "Example Outdoor",
"price": { "amount": 129.95, "currency": "USD", "currencySymbol": "$" },
"rating": { "value": 4.6, "count": 1840 },
"specifications": [{ "displayName": "Color", "values": ["Slate", "Moss"] }],
"tags": ["Waterproof"],
"canTrackPrice": true
}
]
}
],
// Local intent adds map entries.
"map": [
{
"name": "Example Running Co.",
"position": 1,
"placeId": "ChIJexampleBoulder",
"location": { "address": "1200 Example St, Boulder, CO", "latitude": 40.017, "longitude": -105.28 },
"url": "https://www.example-running.com",
"reviews": [{ "count": 312, "rating": 4.8, "providerName": "Example Reviews" }],
"openState": "Open",
"category": "Running store"
}
]
}
}
Cost and request options
No include flag will be needed: once they ship, shoppingCards and map will be part of the Copilot result whenever Copilot shows them. A request costs $0.00274, whatever the answer contains.
The request takes prompt and country; a request that sends state is refused with a 400. Prices and sellers depend on market, so keep it fixed within a series. For local prompts, put the place in the prompt (“running stores in Boulder”).
Example: seller share, brand share and price distribution
Once shopping and map results ship, this script runs a set of product prompts several times and reports seller and brand share of product slots, a top-3 share, the price quartiles per currency, and the map presence rate for a list of your locations.
import os
import statistics
import time
from collections import Counter, defaultdict
from datetime import date
import requests
API = "https://api.answerline.dev"
HEADERS = {"Authorization": f"Bearer {os.environ['API_KEY']}"}
PROMPTS = ["best trail running shoes under $150", "waterproof trail running shoes for winter"]
LOCAL_PROMPTS = ["running shoe stores in Boulder"]
MY_PLACE_IDS = {"ChIJexampleBoulder"}
SAMPLES = 5
DAY = date.today().isoformat()
def run_tasks(tasks):
resp = requests.post(f"{API}/v1/async/task/batch", headers=HEADERS, json=tasks, timeout=60)
resp.raise_for_status()
pending = {item["task"]["id"] for item in resp.json()["results"] if item["success"]}
results = []
while pending:
time.sleep(10)
for task_id in list(pending):
body = requests.get(f"{API}/v1/async/task/{task_id}", headers=HEADERS, timeout=30).json()
status = body["task"]["status"]
if status in ("COMPLETED", "FAILED"):
pending.discard(task_id)
if status == "COMPLETED":
results.append(body["response"]["result"])
return results
def tasks_for(prompts, tag):
return [
{
"taskType": "COPILOT",
"payload": {"prompt": prompt, "country": "US"},
"idempotencyKey": f"{tag}-{p}-s{s}-{DAY}",
}
for p, prompt in enumerate(prompts)
for s in range(SAMPLES)
]
product_answers = run_tasks(tasks_for(PROMPTS, "shoes"))
sellers, brands, top3 = Counter(), Counter(), Counter()
prices = defaultdict(list)
slots = 0
for result in product_answers:
for card in result.get("shoppingCards", []):
for item in card.get("products", []):
slots += 1
sellers[item.get("seller") or "?"] += 1
brands[item.get("brandName") or "?"] += 1
if item.get("position", 99) <= 3:
top3[item.get("brandName") or "?"] += 1
price = item.get("price") or {}
if isinstance(price.get("amount"), (int, float)):
prices[price.get("currency") or price.get("currencySymbol") or "?"].append(price["amount"])
print(f"{len(product_answers)} answers, {slots} product slots")
for name, count in sellers.most_common(10):
print(f"seller {name:30} {count / (slots or 1):5.0%}")
for name, count in brands.most_common(10):
print(f"brand {name:30} {count / (slots or 1):5.0%} top-3 slots {top3[name]}")
for currency, values in prices.items():
if len(values) >= 4:
q1, median, q3 = statistics.quantiles(values, n=4)
print(f"{currency}: n={len(values)} q1={q1:.2f} median={median:.2f} q3={q3:.2f}")
local_answers = run_tasks(tasks_for(LOCAL_PROMPTS, "stores"))
present = [a for a in local_answers if MY_PLACE_IDS & {m.get("placeId") for m in a.get("map", [])}]
ranks = [m["position"] for a in present for m in a["map"] if m.get("placeId") in MY_PLACE_IDS and m.get("position")]
print(f"map presence {len(present)}/{len(local_answers)}", f"mean position {statistics.mean(ranks):.1f}" if ranks else "")
Answers without shopping cards still count in the denominator of “answers”, but add no slots. Report the share of answers that showed any cards next to slot shares, or a week where Copilot shows fewer cards will look like a brand gain.
Pitfalls
Until they ship, shoppingCards and map are absent from every Copilot result. After that they will appear only when Copilot shows them, and product slots vary between runs. Report how often the modules appear, over several samples per period, before reporting who is in them (AI answer volatility).
products[].position runs across all cards. Don’t add a per-card offset.
currency is optional. Group prices by currency, falling back to currencySymbol, and never average across groups.
The same brand can be sold by several sellers, and a marketplace seller can list many brands. Decide which one your client cares about, or report both.
name and brandName are display strings. Use product.groupId or product.brandGroupId, when present, to merge variants of the same product or brand.
map[].price can be any JSON value. Store it raw.
Related
- Copilot engine guide: calls, webhooks, targeting and billing.
- Copilot sources for the pages cited in the same answers.
- Compare with ChatGPT shopping; ChatGPT shopping cards covers the analysis side.
- Competitor analysis and local rank tracking.
Questions
Does the API return Copilot shopping cards today?
Not yet; shopping cards and map results are coming. Today a Copilot result carries the answer text, markdown and its cited sources. When they ship, shoppingCards and map will come with the Copilot result whenever Copilot shows them, at the base price of $0.00274, with no include flag.
Is Copilot product position reset for each shopping card?
No. products[].position will be a 1-indexed rank across all products in all shopping cards in the response, so it can be used directly as a rank without offsetting per card.
Are Copilot prices numbers or strings?
price will be an object with amount as a number, plus currency and currencySymbol. currency is optional, so fall back to currencySymbol when grouping prices.
Which fields identify the seller of a Copilot product?
Each products entry will carry seller and sellerLogoUrl for the retailer, brandName for the manufacturer, offerId and url for the offer itself, and canTrackPrice where price tracking is offered.
Why is there no shoppingCards field in my Copilot result?
Copilot shopping cards and map entries are not returned yet; they are coming. Once they ship, they will appear only when Copilot shows them for that answer: product-intent prompts produce cards far more often than informational ones, and local-intent prompts produce map entries.