Early bird discounts live! Claim your offer

coupon.exhausted

The last remaining use of a capped coupon was taken.

When this fires

A settled redemption took the last remaining use of a capped coupon. Fires immediately after the coupon.redeemed that consumed it, in the same emission.

Use it to close a campaign automatically — pull the code out of a landing page, stop an ad, post "sold out", or notify the creator that the cap they set has been reached.

Only capped coupons can fire this

A coupon with max_redemptions: null is unlimited and never emits this event.

Required ability

project-coupon:view — token must carry this at mint time to subscribe an endpoint to this event.

Payload

{
  "id": "evt_01HX...",
  "type": "coupon.exhausted",
  "created_at": "2026-11-30T21:15:08Z",
  "api_version": "2026-05-01",
  "project_id": "prj_01HX...",
  "data": {
    "coupon_id": "cpn_01HX...",
    "code": "BLACKFRIDAY",
    "max_redemptions": 500,
    "redemptions_count": 500
  }
}

Field reference

FieldTypeNotes
idstring ULIDUnique event id, prefixed evt_. Use for idempotent processing.
typestringAlways coupon.exhausted for this event.
created_atISO 8601 timestampServer-side emission time.
api_versionstringWebhook API contract version.
project_idstring ULIDProject the coupon belongs to.
data.coupon_idstring ULIDThe coupon that ran out.
data.codestringThe code as stored — uppercase.
data.max_redemptionsintegerThe cap that was reached. Never null on this event.
data.redemptions_countintegerSettled redemptions at emission time.

Exhaustion counts live reservations, so it can un-exhaust

This is the one behaviour that surprises people. The quota is enforced against settled redemptions plus reservations whose hold has not expired, so a coupon can report exhausted while the last few uses are still only held by checkouts in progress.

If one of those checkouts is abandoned, the hold expires and its use returns to the pool. The coupon becomes redeemable again — and no event fires for that, because nothing changed on the coupon row.

That design is deliberate: it means an abandoned checkout never permanently burns a use, and no sweeper job is needed to reclaim one. The consequence for you is that coupon.exhausted is a strong signal but not a permanent state. If your automation does something hard to undo — pausing an ad spend, emailing a list — verify the current state with GET /v1/projects/{project}/coupons/{coupon} first, and read redemptions.remaining rather than comparing counts yourself.

`redemptions_count` may be below `max_redemptions`

Because held-but-unsettled uses count towards the cap, the two figures are not guaranteed to be equal on this event. redemptions_count reports settled redemptions only; the difference is the uses currently held by live checkouts.

Caveats

  • It can fire more than once for the same coupon. Exhausted → a hold expires → redeemable → exhausted again is a legitimate sequence. De-dup on the event id, not on the coupon id.
  • A creator raising max_redemptions does not emit a matching "un-exhausted" event. That is a coupon.updated with max_redemptions in its changes.
  • Expiry is a different thing entirely. A coupon that runs past expires_at stops applying without ever being exhausted, and emits nothing.

How is this guide?

On this page

Subscriby is a product designed by you — for you.

No boardroom full of executives deciding what we ships next. Our roadmap always shaped by you with your feedback.

Share feedback or a request