Early bird discounts live! Claim your offer

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

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.

How is this guide?

On this page

Subscriby is a product designed by you — for you.

No boardroom full of executives deciding what we ships next. Our roadmap always shaped by you with your feedback.

Share feedback or a request