Coupons: expiry and status lifecycle
This document explains when a coupon's expiry date is set, how it is calculated, and the statuses a coupon moves through in Mambo.
Two levels: the coupon and the user's coupon
Mambo keeps these separate, and most confusion about coupon status comes from mixing them up.
The coupon is the offer you configure: its name, its value, its code or codes, how many exist, and its expiry rules. It lives in your catalogue.
The user's coupon is one instance of that offer, held by one person. Each time someone earns or buys the coupon, a new entry is created against their record with its own award date, its own expiry date, and its own redemption history. If the same person earns the same coupon three times, they hold three independent entries.
Expiry settings are configured on the coupon. Expiry dates are stored on the user's coupon.
How the expiry date is calculated
Each coupon has an expiry setting, and it determines what gets written to expiryOn when someone receives the coupon.
| Expiry setting | Value stored in expiryOn |
|---|---|
| None | Empty. That coupon never expires. |
| A fixed date, for example 31 December 2026 | That same date, for everyone, regardless of when they received it. |
| A relative period, for example 30 days after | The award date plus 30 days. |
With a relative period, two people who earn the same coupon a week apart get two different expiry dates. This is intentional: everyone gets the same window from the moment they receive it.
When the expiry date is set
expiryOn is written at the moment the coupon is given to the user, whether that happened because a rule awarded it or because the user bought it with points. It is stored alongside awardedOn and it does not change afterwards.
Two consequences worth noting:
- Redeeming a coupon does not touch it. Redemption adds a timestamp to
redeemedOnand leavesexpiryOnexactly as it was. - Expiring does not write it either. The date was already there. Expiry is simply that date being reached.
Because it is fixed at award time, changing a coupon's expiry setting later does not retroactively change coupons already in users' hands. Existing entries keep the expiry date they were given. The new setting applies from that point on.
Status of a coupon in your catalogue
A coupon has no single status field. Its availability is the combination of the following.
Active or inactive
Your on and off switch. While a coupon is inactive nobody can earn it, buy it or redeem it, and it disappears from the buyable coupon list.
Expired
If you gave the coupon a fixed calendar date, the offer itself stops working for everyone once that date passes.
A relative expiry such as "30 days after" does not do this. It applies to each user's copy, not to the offer, so the offer keeps running indefinitely and hands a fresh window to each new recipient. Only a fixed date retires the offer as a whole.
Out of stock
If the coupon has a limited quantity, or a per user limit, requests to earn or buy it are refused once the limit is reached. This is separate from expiry and from the active flag.
Status of a user's coupon
Mambo derives the status of each entry from three stored values: awardedOn, redeemedOn and expiryOn.
Available
The user holds the coupon, has not used it, and it has not expired. Every coupon starts here, whether it was awarded by a rule or bought with points. The coupon code is assigned at this moment.
Redeemed
The user has used the coupon, and the time of use is recorded in redeemedOn.
For a single use coupon this is the end of the line. For a multi use coupon the entry stays available and each use is appended, so redeemedOn is a list of timestamps rather than a single one.
Expired
The date in expiryOn has passed.
Expiry overrides everything else. An expired multi use coupon cannot be redeemed even if it had uses remaining.
Refunded
A redemption can be reversed. For a normal coupon this removes the most recent use and returns the entry to Available, so the user can redeem it again.
For a coupon that was bought with points, a refund removes the entry from the user altogether and returns its code to the pool for someone else.
Cancelled
If the reason a user earned the coupon stops being true, for example a KPI target they had reached is no longer met, Mambo withdraws the coupon and releases its code. This only ever applies to a coupon that has not been used.
Summary
Available becomes Redeemed or Expired. A refund steps back from Redeemed to Available. A cancellation removes an unused coupon entirely.
How expiry takes effect
A coupon stops being redeemable the second its expiry date passes. Mambo compares expiryOn against the current time on every redemption attempt, so there is no window in which an expired coupon can still be used.
Separately, a background job runs every five minutes to mark newly expired coupons as expired and raise the CouponStateExpired event, which is what delivers the webhook. This is what drives notifications and reporting. It never affects whether a coupon can be redeemed, since that is already settled by the timestamp.
In practice: a coupon that expires at 14:02 is unusable at 14:02, and the corresponding webhook arrives within five minutes.
What expiry does not do
Expiry is not a reversal. When a coupon expires:
- Points spent to buy it are not refunded.
- The coupon code is not returned to the pool for another user.
If you want users compensated for coupons they let lapse, that has to be handled in your own systems, using the CouponStateExpired webhook as the trigger.
Webhooks
| Event | Webhook |
|---|---|
| Coupon awarded to a user | CouponStateAdded |
| Coupon redeemed | CouponStateRedeemed |
| Coupon expired | CouponStateExpired |
Refunds and cancellations do not currently emit a webhook.
Reporting on status
When listing coupon states through the API you can filter by status with withRedeemableOnly, withRedeemedOnly and withExpiredOnly. These use exactly the definitions above: redeemable means not yet used and not expired, redeemed means it has at least one redemption, expired means its expiry date has passed.