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

Coupon savings

Computed savings rating for one coupon. Use to surface a quality badge ('great deal', 'okay') without recomputing client-side.

GET/v1/coupons/{coupon_id}/savings$0.01

Path parameters

NameTypeDescription
coupon_idrequireduuidCoupon id from /v1/coupons.

Request

Request
curl 'https://api.mainmarket.com/v1/coupons/0bc5b1d4-9a25-4d70-8a82-2ab3f6f1d901/savings'

Response

200 OK (computable)json
{
  "coupon_id": "0bc5b1d4-9a25-4d70-8a82-2ab3f6f1d901",
  "savings_pct": 22.27,
  "rating": "good",
  "computable": true,
  "reason": null
}
200 OK (not computable)json
{
  "coupon_id": "5e1a2b3c-4d5e-6f70-8901-2a3b4c5d6e7f",
  "savings_pct": null,
  "rating": null,
  "computable": false,
  "reason": "missing_catalog_price"
}

Response fields

NameTypeDescription
coupon_iduuidEcho of the requested coupon id.
savings_pctnumber | null0.0–100.0. Null when computable=false.
ratingstring | nullOne of great, good, okay, poor. Null when computable=false.
computablebooleanTrue when we have a baseline price to compute savings against.
reasonstring | nullDiagnostic when computable=false. Common values: missing_catalog_price, missing_discount_value, unknown_discount_type, expired.
ℹ
Rating thresholds
great ≥ 50%, good 20–50%, okay 10–20%, poor< 10%. Thresholds are deliberately conservative for grocery — anything above 50% off the regular price is rare outside of clearance and usually indicates either a clipper-only loyalty deal or an outlier that earned the "great" badge but should be sanity-checked before display.

When to use this vs the inline savings on /v1/coupons

Every row from List coupons already carries savings_pct, rating, total_savings, and deal_price inline. So when do you call this dedicated route?

Refresh after a clip
After a user clips a coupon in your UI, you can re-fetch this single row to confirm the savings are still computable (the source SPP price might have moved). Cheaper than re-pulling the whole coupon list.
Quality badging in a non-coupon context
If you're rendering a product detail page that shows attached coupons (via Coupons for product), you already have the coupon ids — call this route to decorate each with a badge without re-fetching the parent coupon's full body.
Diagnostic when a coupon shows up unranked
When the parent /v1/coupons row has savings_pct: null, this endpoint's reason field tells you why — useful for support tickets or QA dashboards.

How savings are computed by discount type

The savings math depends on discount_type on the parent coupon. The formula is the same one used to populate savings_pct on the inline /v1/coupons response — exposing it here so you can audit or reproduce client-side:

dollar_off
savings_pct = discount_value / catalog_price × 100. e.g. $1 off a $4.49 product = 22.27%.
percent_off
savings_pct = discount_value directly (already a percentage).
bogo (buy N, get M free)
savings_pct = bogo_get_qty / (min_purchase_qty + bogo_get_qty) × 100. Buy 1 get 1 = 50%; buy 4 get 1 = 20%. Returns computable=false when both legs aren't priced.
bundle / loyalty_price
savings_pct = (catalog_price − bundle_unit_price) / catalog_price × 100. Requires both the bundle's effective unit price and the regular catalog price to be present.
featured
computable=false always — featured coupons are a marketing surface in the chain's app, not an actual discount.

Workflow: badge a coupon list with quality ratings

Request
import httpx

# Step 1: pull a list (savings inline). Use this most of the time.
list_resp = httpx.get(
    "https://api.mainmarket.com/v1/coupons",
    params={"chain_id": "12d6...", "limit": 50},
    headers={"Authorization": "Bearer mm_live_..."},
).json()

# Step 2: only call /savings when you want a fresh, focused decoration
# (e.g. after a user clip event invalidates the cached rating)
def fetch_rating(coupon_id):
    r = httpx.get(
        f"https://api.mainmarket.com/v1/coupons/{coupon_id}/savings",
        headers={"Authorization": "Bearer mm_live_..."},
    ).json()
    if not r["computable"]:
        return None, r.get("reason")
    return r["rating"], None

for c in list_resp["results"][:10]:
    rating, reason = fetch_rating(c["id"])
    badge = {"great": "🔥", "good": "✓", "okay": "·", "poor": "·"}.get(rating, "?")
    note = f" ({reason})" if reason else ""
    print(f"{badge}  {c['product_name']}  {c['value_text']}{note}")
ℹ
Don't decorate every row this way
Calling /savings for every coupon in a 50-row list is 50 paid calls. The inline savings_pct on /v1/coupons is one paid call total. Only use the dedicated route for targeted refreshes (post-clip, post-edit, deep-link landing).

UI rendering tips

  • Map rating to a color: great=green, good=blue, okay=neutral, poor=muted-gray. Don't show "poor" coupons in red — they're not bad, just low-savings.
  • When computable=false, hide the badge entirely rather than showing "?" — the absence of a rating is meaningful information for shoppers but rarely something they want to think about.
  • missing_catalog_price is the most common reason coupons aren't computable. It usually clears within a week as the product gets scraped at the coupon's home chain — worth re-fetching periodically.

Notable behavior

  • Savings are computed against catalog_price when available, otherwise against the live store_product_prices.regular_price at the coupon's home chain. The basis is exposed via total_savings_basis on the parent coupon row (one of catalog_price, store_price, or null).
  • BOGO and bundle coupons require both legs to be priced — computable=false when only one leg has a baseline. The reason field will be missing_catalog_price in this case.
  • The endpoint is read-only against the same materialized view the coupon list uses — calling it does not trigger a recompute. If a coupon's ratings look stale, the source data hasn't refreshed yet.
  • No cache headers on this route — call it whenever you want a fresh read. The underlying view refreshes hourly so back-to-back calls return identical results.

Related

  • List coupons — the inline source of savings_pct on every row. Start here unless you have a focused reason not to.
  • Get a coupon — full coupon body for a single coupon (one row per matched product).
  • Coupons for product — every active coupon for one canonical product. Pair with this route to badge each.

Errors

NameTypeDescription
404Not Foundcoupon_id does not exist.
402Payment RequiredPaid route — no payment proof.