Fetch one coupon by id, expanded into all matching (coupon, product) rows. Used to refresh a single deal after a clip event, deep-link a shareable coupon URL, or hydrate the body of a coupon detail screen.
/v1/coupons/{coupon_id}$0.01CouponRow shape — see List coupons for the full field reference. This page focuses on what's different about the single-id workflow.savings_pct, deal_price, and is_clippable state without paying for an entire list refresh./coupons/0bc5... in your app should hydrate from this endpoint, not from the cached list — the list might be stale or the deep link might predate the cache.| Name | Type | Description |
|---|---|---|
coupon_idrequired | uuid | Coupon id from /v1/coupons. Stable forever — won't change on the next ingestion run. |
curl 'https://api.mainmarket.com/v1/coupons/0bc5b1d4-9a25-4d70-8a82-2ab3f6f1d901'{
"count": 2,
"results": [
{
"id": "0bc5b1d4-9a25-4d70-8a82-2ab3f6f1d901",
"chain_id": "12d6...",
"chain_name": "HEB",
"product_id": "9d4e...",
"product_name": "Cheerios Cereal, 12 oz",
"discount_type": "dollar_off",
"discount_value": 1.50,
"min_purchase_qty": 2,
"valid_from": "2026-04-29",
"valid_to": "2026-05-13",
"savings_pct": 13.65,
"deal_price": 4.74,
"role": "buy"
},
{
"id": "0bc5b1d4-9a25-4d70-8a82-2ab3f6f1d901",
"product_id": "8a1f...",
"product_name": "Cheerios Cereal, 18 oz",
"discount_type": "dollar_off",
"discount_value": 1.50,
"min_purchase_qty": 2,
"savings_pct": 11.91,
"deal_price": 5.49,
"role": "buy"
}
]
}The response wrapper { count, results } lets one coupon expand into many rows. Two cases produce more than one row:
id, chain_id, and discount metadata, but a different product_id + per-product savings math.buy and get legs as separate rows, each with role: "buy" or role: "get". Pair them in your UI by coupon id — the buy row shows the qualifying product, the get row shows the freebie.id must use the (id, product_id) tuple instead. Coupons with zero matched canonical products are suppressed entirely — you'll never see an empty results array for a real coupon id (you'd get a 404 instead).import httpx
coupon_id = "0bc5b1d4-9a25-4d70-8a82-2ab3f6f1d901"
r = httpx.get(
f"https://api.mainmarket.com/v1/coupons/{coupon_id}",
headers={"Authorization": "Bearer mm_live_..."},
).json()
if not r["results"]:
raise ValueError("Coupon has no matched products")
# Header — pull from any row (coupon-level fields are identical)
header = r["results"][0]
print(f"{header['chain_name']}: {header['description']}")
print(f"Valid {header['valid_from']} - {header['valid_to']}")
# Per-product breakdown
print("\nApplies to:")
for row in r["results"]:
role = f" [{row['role']}]" if row.get("role") and row["role"] != "buy" else ""
print(f" • {row['product_name']} ${row['deal_price']:.2f} ({row['savings_pct']:.0f}% off){role}")results array client-side.chain_name, valid_to, description are identical across all rows of a multi-row response. Read them from results[0] and don't iterate./v1/prices, coupons aren't fallback-resolved across stores — a coupon either applies at its chain_id (or store_id if store-scoped) or it doesn't.| Name | Type | Description |
|---|---|---|
404 | Not Found | coupon_id does not exist. |
402 | Payment Required | Paid route — no payment proof. |