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.
Background
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.
Events
coupon.createdA new coupon code was authored in a project.coupon.updatedA coupon's terms changed, with a field-by-field list of what moved.coupon.activatedA coupon was switched on and is being offered at checkout again.coupon.deactivatedA coupon was switched off and stops being offered at checkout.coupon.deletedA coupon was removed, carrying its final snapshot.coupon.redeemedA subscriber's discounted payment settled and the redemption was recorded.coupon.exhaustedThe last remaining use of a capped coupon was taken.Related
How is this guide?
Last updated on