coupon.* events
Coupon code lifecycle and redemption.
Fires as coupon codes are authored, switched on and off, deleted, and redeemed at checkout. A coupon is one code many subscribers can redeem for money off — the multi-redemption counterpart to an access code, which is one code for one person and grants access outright.
Five of these describe the code itself and are driven by the creator. Two describe money:
coupon.redeemed fires once per settled checkout, and coupon.exhausted fires the moment the
last remaining use is taken.
Events
coupon.created
A new coupon code was authored.
coupon.updated
A coupon's terms changed, with a list of what moved.
coupon.activated
A coupon was switched on and is being offered again.
coupon.deactivated
A coupon was switched off and stops being offered.
coupon.deleted
A coupon was removed, carrying its final snapshot.
coupon.redeemed
A subscriber's discounted payment settled.
coupon.exhausted
The last remaining use was taken.
The lifecycle events nest a coupon snapshot
created, updated, activated, deactivated and deleted all carry the same nested object
under data.coupon, so one consumer can handle all five:
{
"coupon": {
"id": "cpn_01HX...",
"code": "BLACKFRIDAY",
"name": "Black Friday 2026",
"discount_type": "percentage",
"discount_value": "25.0000",
"currency_id": null,
"duration": "once",
"max_redemptions": 500,
"max_redemptions_per_user": 1,
"minimum_amount": null,
"starts_at": "2026-11-27T00:00:00+00:00",
"expires_at": "2026-12-01T00:00:00+00:00",
"active": true
}
}The snapshot deliberately omits redemption counts
Counts move independently of the coupon row, so a lifecycle event quoting one
would be reporting a figure that was already stale by the time the webhook
landed. Use coupon.redeemed for counts,
or read the current figure from GET /v1/projects/{project}/coupons/{coupon} .
coupon.redeemed and coupon.exhausted do not use this shape — they are flat, because they
describe a transaction rather than the code.
discount_value is a decimal string
It is sent as a string, not a number, and always carries four decimal places: "25.0000" for
25% off, "10.0000" for ten of the coupon's currency. Parse it as a decimal, not a float —
money arithmetic on a float is how a $9.99 charge becomes $9.98.
currency_id is null on a percentage coupon and set on a fixed-amount one, because a
percentage applies to a plan in any currency and a fixed amount only to plans priced in its own.
An empty plan restriction means every plan
The lifecycle payloads do not carry the plan restriction list. If you need it, read the coupon
from the API — and read plan_ids: [] as applies to every plan in the project, including plans
added later. It does not mean applies to nothing.
Required ability
Every event in this family requires project-coupon:view at token mint time. See
Abilities.
Related events
access_code.*— one code, one person, grants access.payment.*— the payment a redemption rode in on.subscription.*— the subscription the discount bought.
How is this guide?