coupon.created
A new coupon code was authored in a project.
When this fires
A creator authored a new coupon code — in the dashboard, through the Telegram bot, over the REST
API, or via the create_coupon MCP tool. All four routes emit the same event.
It fires on authoring, not on availability. A coupon created with a future starts_at fires
this immediately and only becomes redeemable later, so do not read this as "a discount is now
live" — check starts_at and active.
There is no separate event for a coupon becoming redeemable when its
starts_at passes. Schedule against starts_at from this payload if you need
that moment.
Required ability
project-coupon:view — token must carry this at mint time to subscribe an endpoint to this event.
Payload
{
"id": "evt_01HX...",
"type": "coupon.created",
"created_at": "2026-11-20T09:14:02Z",
"api_version": "2026-05-01",
"project_id": "prj_01HX...",
"data": {
"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
}
}
}Field reference
| Field | Type | Notes |
|---|---|---|
id | string ULID | Unique event id, prefixed evt_. Use for idempotent processing. |
type | string | Always coupon.created for this event. |
created_at | ISO 8601 timestamp | Server-side emission time. |
api_version | string | Webhook API contract version. |
project_id | string ULID | Project the coupon belongs to. |
data.coupon.id | string ULID | The coupon, prefixed cpn_. |
data.coupon.code | string | The redeemable string. Always uppercase — stored and compared that way. |
data.coupon.name | string | Creator-facing label. Never shown to subscribers. |
data.coupon.discount_type | percentage | fixed | Whether discount_value is a percent or an amount. |
data.coupon.discount_value | decimal string | Four decimal places, e.g. "25.0000". Parse as a decimal, never a float. |
data.coupon.currency_id | string ULID | null | null on a percentage coupon; set on a fixed-amount one. |
data.coupon.duration | once | Which charges the discount applies to. Only once — the first payment — is authorable. |
data.coupon.max_redemptions | integer | null | Total cap across everyone. null means unlimited. |
data.coupon.max_redemptions_per_user | integer | Per-subscriber cap. Always at least 1. |
data.coupon.minimum_amount | decimal string | null | Order floor in the plan's own currency. null means no floor. |
data.coupon.starts_at | ISO 8601 | null | When it becomes redeemable. null means immediately. |
data.coupon.expires_at | ISO 8601 | null | When it stops. null means it runs until switched off. |
data.coupon.active | boolean | Whether the creator has it switched on. |
Caveats
- The snapshot carries no redemption counts and no plan restriction. Counts move
independently of the coupon row, and the plan list is a separate relation. Read both from
GET /v1/projects/{project}/coupons/{coupon}if you need them. - A coupon can be created already switched off.
active: falsehere is legitimate, not a race — a creator can author a code ahead of a campaign and switch it on later, which emitscoupon.activated. codeis uppercase regardless of what was typed. A creator enteringblackfridaygetsBLACKFRIDAY. Compare case-sensitively against the uppercase form.- Timestamps inside
data.couponare ISO 8601 with offset (+00:00), unlike the envelope'screated_at, which usesZ. Both are UTC.
Related events
coupon.updated— the same snapshot after a change, plus a list of what moved.coupon.activated/coupon.deactivated— availability flips.coupon.redeemed— a subscriber actually used it.- Coupon events overview — back to family overview.
How is this guide?