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
| Field | Type | Notes |
|---|---|---|
id | string ULID | Unique event id, prefixed evt_. Use for idempotent processing. |
type | string | Always coupon.exhausted for this event. |
created_at | ISO 8601 timestamp | Server-side emission time. |
api_version | string | Webhook API contract version. |
project_id | string ULID | Project the coupon belongs to. |
data.coupon_id | string ULID | The coupon that ran out. |
data.code | string | The code as stored — uppercase. |
data.max_redemptions | integer | The cap that was reached. Never null on this event. |
data.redemptions_count | integer | Settled 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_redemptionsdoes not emit a matching "un-exhausted" event. That is acoupon.updatedwithmax_redemptionsin itschanges. - Expiry is a different thing entirely. A coupon that runs past
expires_atstops applying without ever being exhausted, and emits nothing.
Related events
coupon.redeemed— the redemption that consumed the last use; always precedes this.coupon.updated— where a raised cap shows up.coupon.deactivated— switched off by decision rather than run out by use.- Coupon events overview — back to family overview.
How is this guide?