Coupons
Create discount codes for win-back campaigns, launch offers, and referrals. Mirrored to Stripe automatically.
Coupons are codes your users redeem at checkout. Each code is stored on crxpay and mirrored to your connected Stripe account the moment you create it, so there's no extra round-trip at purchase time.
Create a coupon
Open Coupons in the dashboard sidebar and click New coupon.
| Field | Notes |
|---|---|
| Code | Uppercase letters, digits, _ or -. Shown to the user. |
| Type | Percentage off, Fixed amount off (cents), or Free first N months |
| Amount | 1–100 for percentage, cents for fixed, 1–36 for months |
| Repeat for | How many months the discount repeats. Blank means one-time. Ignored for first_n_months. |
| Max redemptions | Optional cap. After this many uses, the code stops working. |
| Expires | Optional cut-off date. |
On create, crxpay calls Stripe's Coupons API on your connected account and stores the resulting coupon ID. If the Stripe call fails, the local coupon is not created — you'll see the Stripe error and can retry.
Redeem a coupon from the SDK
Pass the code to showPaywall or openCheckout:
await crxpay.showPaywall({ plan: 'pro_monthly', coupon: 'WINBACK30' });
The paywall UI should allow the user to enter their own code as well. Validate a user-entered code before showing it as applied:
// Inside your paywall UI
const res = await fetch('https://api.crxpay.io/v1/sdk/coupons/validate', {
method: 'POST',
headers: { 'Authorization': `Bearer ${publicApiKey}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ code: userInput }),
});
const data = await res.json();
// data.code, data.type, data.amount, data.durationMonths, data.expiresAt
If the code is archived, expired, or out of redemptions, the endpoint returns 404 invalid_or_expired.
Detect an active discount
When a subscription has a coupon applied, the SDK exposes it on sub.discount:
const sub = await crxpay.ready();
if (sub.discount) {
// { code: 'WINBACK30', type: 'percentage', amount: 30, endsAt: Date | null }
showBanner(`Your ${sub.discount.code} discount runs until ${sub.discount.endsAt}`);
}
endsAt is null for coupons that last the lifetime of the subscription.
Webhooks
When a user completes checkout with a valid code, you receive a coupon.redeemed webhook:
{
"type": "coupon.redeemed",
"data": {
"mode": "live",
"code": "WINBACK30",
"type": "percentage",
"amount": 30,
"processorSubscriptionId": "sub_1Q..."
}
}
Redemptions are deduplicated per (coupon, subscription) — even if Stripe re-delivers the customer.subscription.created event, you'll only see one coupon.redeemed webhook per subscription.
Win-back flow
- A user cancels. You receive
subscription.cancelled. - Send them an email (or show an in-extension banner on next open) with a code like
COMEBACK40. - They click →
crxpay.showPaywall({ coupon: 'COMEBACK40' }). - They check out. You receive
coupon.redeemedandsubscription.created.
The Coupons dashboard page shows redemption counts per code so you can measure each campaign.
Was this page helpful?
Your feedback shapes what we document next.