MainMarketAPI Reference
Get Access
Overview
  • Introduction
  • Authentication
  • Errors
Stores
  • List stores
  • Get a store
  • Store sentiment
  • Store coupons
  • Store aisles
Chains
  • List chains
  • Get a chain
  • Chain aisles
  • Resolve a list
Products
  • Catalog search
  • Get a product
  • Coupons for product
Prices
  • Search prices
  • Prices by UPC
  • Prices at a store
  • Cheapest nearby
Coupons
  • List coupons
  • Get a coupon
  • Coupon savings
Indices
  • Published indices
Discovery & meta
  • Discovery routes
  • OpenAPI spec
  • Agent skill spec
Overview
  • Introduction
  • Authentication
  • Errors
Stores
  • List stores
  • Get a store
  • Store sentiment
  • Store coupons
  • Store aisles
Chains
  • List chains
  • Get a chain
  • Chain aisles
  • Resolve a list
Products
  • Catalog search
  • Get a product
  • Coupons for product
Prices
  • Search prices
  • Prices by UPC
  • Prices at a store
  • Cheapest nearby
Coupons
  • List coupons
  • Get a coupon
  • Coupon savings
Indices
  • Published indices
Discovery & meta
  • Discovery routes
  • OpenAPI spec
  • Agent skill spec

Coupons for product

Every active coupon that applies to one canonical product, across every chain we track. Matches both single-UPC coupons (1:1 link) and multi-UPC bundle coupons (m:n link), dedupes by coupon id, and sorts by savings descending. The 'should I render a deal badge on this product page?' query.

GET/v1/products/{product_id}/coupons$0.01
ℹ
Product-detail page pattern
Use this when you have a canonical product_id in hand (from a UPC scan, search result, or basket entry) and want to surface every active deal attached to it — independent of which chain or store the user shops at. For the inverse question ("what coupons are at this store right now?"), List coupons with a store_id filter is the right call.

Path parameters

NameTypeDescription
product_idrequireduuidCanonical product id.

Query parameters

NameTypeDescription
valid_ondateISO date YYYY-MM-DD. Defaults to today.
limitintegerdefault: 501–200.
offsetintegerdefault: 0Cursor offset.
ℹ
Bundle coupons (e.g. "spend $20 on cereal") appear here for every product in the bundle. The same coupon id may show up against many products in a basket-pricing pass.

Request

Request
curl 'https://api.mainmarket.com/v1/products/9d4e1c80-78a3-4a6d-9b1f-2cdb3d0c7e90/coupons'

Response

200 OKjson
{
  "count": 1,
  "results": [
    {
      "id": "0bc5b1d4-9a25-4d70-8a82-2ab3f6f1d901",
      "chain_id": "12d6...",
      "chain_name": "HEB",
      "chain_logo_url": "https://.../heb.png",
      "store_id": null,
      "product_id": "9d4e1c80-78a3-4a6d-9b1f-2cdb3d0c7e90",
      "product_name": "Cheerios Cereal, 18 oz",
      "product_brand": "Cheerios",
      "product_image_url": "https://.../cheerios.jpg",
      "discount_type": "dollar_off",
      "discount_value": 1.50,
      "min_purchase_qty": 2,
      "description": "Save $1.50 on 2 boxes of Cheerios",
      "fine_print": "Limit 4 per transaction",
      "valid_from": "2026-04-29",
      "valid_to": "2026-05-13",
      "is_digital_only": true,
      "requires_loyalty": false,
      "is_clippable": true,
      "savings_pct": 13.65,
      "rating": "good",
      "deal_price": 4.74,
      "role": "buy"
    }
  ]
}

Each row uses the standard CouponRow shape — see List coupons for the full field reference. This page focuses on what's specific to the product-scoped flow.

How matching works

Two database paths feed the result, then we union and dedupe by coupon id:

1:1 product-coupon link
Hits coupons.product_id directly. Used for coupons that target a specific UPC ("$1.50 off Cheerios 18oz"). One row per matching coupon.
m:n bundle link
Hits coupon_products.product_id — the join table that attaches multi-product coupons to each member product. Used for brand- family deals ("$1 off any Cheerios cereal"), bundle coupons ("spend $20 on cereal, save $5"), and BOGO coupons.
Dedup pass
A coupon that matches via both paths only appears once. The result is always one row per (coupon, product) pair — never duplicate coupon ids for the same product.

Workflow: product detail page deal section

Request
import httpx

# User landed on a product detail page; pull every active coupon
product_id = "9d4e1c80-78a3-4a6d-9b1f-2cdb3d0c7e90"
r = httpx.get(
    f"https://api.mainmarket.com/v1/products/{product_id}/coupons",
    headers={"Authorization": "Bearer mm_live_..."},
).json()

if not r["results"]:
    print("No active deals for this product")
else:
    print(f"{len(r['results'])} deals available:")
    for c in r["results"]:
        # Skip BOGO get-legs (the freebie row); show only the buy-leg / single
        if c.get("role") == "get":
            continue
        store_scope = c["chain_name"] if c["store_id"] is None else f"{c['chain_name']} (store-only)"
        until = c["valid_to"] or "no expiry"
        rating = c.get("rating", "?")
        print(f"  [{rating:>5}] {c['description']}  — {store_scope}, exp {until}")

Workflow: badge a basket UI

For a multi-item shopping list, you typically want "🏷 deal" indicators next to items that have any active coupon. Fan out one call per product (cheap — most products have 0-2 coupons), or use List coupons with a chain filter and join client-side.

Per-item fanoutpython
import httpx

basket = ["9d4e...", "a1b2...", "c3d4..."]
deals_for = {}
for pid in basket:
    r = httpx.get(
        f"https://api.mainmarket.com/v1/products/{pid}/coupons",
        params={"limit": 5},
        headers={"Authorization": "Bearer mm_live_..."},
    ).json()
    deals_for[pid] = r.get("count", 0)

for pid, n in deals_for.items():
    badge = f"🏷 {n}" if n else ""
    print(f"  {pid[:8]}...  {badge}")

Notable behavior

  • Sort. savings_pct DESC primary, valid_to ASC tiebreaker (soonest-to-expire wins ties). Best-deal- first ordering matches the standard product-detail UI.
  • BOGO get-legs. When a coupon's role is get, the implied unit price is zero — it's the "free" half of a buy-one-get-one. Surface carefully in UI; pair with the matching buy row by coupon id, or filter role !== "get" to show only the qualifying side.
  • Cross-chain dedup. A coupon valid at HEB and a different coupon valid at Wegmans for the same product return as two distinct rows. Each row's chain_name tells you where it's redeemable.
  • Bundle coupons appear once per member. A "$5 off when you spend $20 on cereal" bundle returns one row for every cereal in the bundle — which means this product page might show the same bundle id repeating across its sibling products' pages.
  • No geo filter. Unlike List coupons, this endpoint doesn't take lat/lng — every active coupon attached to the product comes back regardless of geography. Use the chain_id on each row to filter to the user's home metro client-side.

Caching

Coupon-to-product attachments change daily as new deals get ingested and old ones expire. The endpoint sends Cache-Control: public, max-age=300, stale-while-revalidate=600 — a 5-minute cache. Don't cache longer — coupons can flip between clipped/unclipped state during a single session.

Related

  • List coupons — chain-scoped or store-scoped coupon search. Use this when you have a store id, not just a product id.
  • Get a coupon — single coupon detail (when you want the full body, including all matched products).
  • Coupon savings — focused savings + rating endpoint for one coupon. Useful for badge decoration after a clip event.
  • Get a product — the canonical product whose coupons this endpoint returns.

Errors

NameTypeDescription
404Not Foundproduct_id does not exist.
422Unprocessable EntityBad valid_on date format.
402Payment RequiredPaid route — no payment proof.