coupon.exhausted

The last remaining use of a capped coupon was taken.

Ability to subscribeproject-coupon:view

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.

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 the coupon endpoint first, and read redemptions.remaining rather than comparing counts yourself.

redemptions_count can exceed max_redemptions. The counter is bumped when a checkout takes a hold, not when it settles, and is only decremented when a hold is explicitly released. Lapsed holds the stale-reservation sweep has not yet reclaimed still sit in it, so on this event redemptions_count is equal to or greater than max_redemptions, never below it. Read redemptions.remaining from the API if you need the reservation-aware figure.

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.

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.

Header Parameters

SB-Signature*string

t=<unix seconds>,v1=<hex>: the HMAC-SHA256 of "<t>.<raw body>" under the endpoint's secret. Verify it before acting, and refuse a t more than 300 seconds from now. During a secret rotation a v0= signature under the previous secret may precede v1=.

SB-Event-Id*string

The event's ULID, bare. The envelope's id is the same ULID prefixed evt_, so strip the prefix before comparing. Deduplicate on it: a retry carries the same id.

SB-Event-Name*string

The event name, the same as the envelope's type.

Content-Type*string

Always application/json.

User-Agent*string

Always Subscriby-Webhooks/1.0.

Request Body

application/json

The signed JSON envelope posted to your endpoint.

TypeScript Definitions

Use the request body type in TypeScript.

The envelope every event is delivered in.

Response Body

Example Requests

POST/coupon.exhausted

How is this guide?