coupon.* events
Coupon code lifecycle and redemption.
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.
| Event | Fires when |
|---|---|
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": "4b9d3e08-a1f6-4275-9c83-7e01d6a2f594",
"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.redeemedfor counts, or read the current figure from the coupon endpoint.
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. On coupon.updated, coupon.activated, coupon.deactivated and coupon.deleted it is read back from the database and carries four decimal places ("25.0000"); on coupon.created it is the value exactly as submitted ("25"), because that payload is built before any round trip. minimum_amount follows the same rule. Parse it as a decimal either way, never 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.
Related
access_code.*: one code, one person, grants access.payment.*: the payment a redemption rode in on.subscription.*: the subscription the discount bought.
Events
coupon.created
A new coupon code was authored in a project.
coupon.updated
A coupon's terms changed, with a field-by-field list of what moved.
coupon.activated
A coupon was switched on and is being offered at checkout again.
coupon.deactivated
A coupon was switched off and stops being offered at checkout.
coupon.deleted
A coupon was removed, carrying its final snapshot.
coupon.redeemed
A subscriber's discounted payment settled and the redemption was recorded.
coupon.exhausted
The last remaining use of a capped coupon was taken.
Compare with the current pages
How is this guide?