Skip to content

Coupons ​

Coupons allow you to offer discounts during checkout. They support both percentage and fixed-amount discounts, with optional constraints on validity period, redemption limits, and applicable plans.

Coupon Structure ​

FieldTypeDescription
codestringUnique coupon code entered by users
discountType"percentage" | "fixed"Type of discount
discountValuenumberDiscount amount (0-100 for percentage, absolute for fixed)
currencystringCurrency for fixed-amount discounts
maxRedemptionsnumberMaximum number of times this coupon can be used (null = unlimited)
currentRedemptionsnumberNumber of times redeemed so far
validFromDateCoupon becomes valid at this date
validUntilDateCoupon expires at this date
benefitDurationUnit"days" | "months"Unit for the post-redemption benefit duration (unset = benefit lasts for the life of the subscription)
benefitDurationValuenumberLength of the post-redemption benefit, in benefitDurationUnit units
applicablePlanIdsstring[]Plans this coupon works with (empty = all plans)
isActivebooleanWhether the coupon is currently active

Redemption window vs. benefit duration ​

These two concepts are easy to confuse, but they govern different things:

  • Redemption window (validFrom / validUntil) — gates whether the coupon can currently be redeemed at all. A coupon with validUntil in the past can no longer be applied at checkout, regardless of its discount.
  • Benefit duration (benefitDurationUnit / benefitDurationValue) — once a coupon has been redeemed, this controls how long the discount/free period lasts for that subscriber. For example, a coupon with benefitDurationUnit: 'months' and benefitDurationValue: 3 gives the redeeming subscriber 3 months at the discounted price; after that, billing automatically reverts to the plan's standard price — the subscription itself is not canceled.

If benefitDurationUnit/benefitDurationValue are left unset, the discount applies for the life of the subscription, matching the pre-existing behavior.

When a coupon with a benefit duration is redeemed (subscription activation), the end date of the benefit is computed from the subscription's currentPeriodStart (falling back to the redemption time) and stored on the subscription as couponBenefitEndsAt. Pricing logic checks this field — if the current date is past couponBenefitEndsAt, the standard plan price is used instead of the discounted one.

Admin Panel ​

The coupon create/edit form (Pricing Plans → Coupons in the admin panel) has a Benefit Duration section, separate from the Redemption Limits section's Valid From/Valid Until date pickers:

  • Duration — a numeric field for benefitDurationValue. Leave empty for the discount to apply for the life of the subscription.
  • Duration Unit — a Days/Months dropdown for benefitDurationUnit, enabled once a duration value is entered.

A helper note in the form clarifies the distinction: "How long the discount lasts after a customer redeems this coupon (leave blank for the life of the subscription)" — as opposed to Valid From/Valid Until, which only gate whether the coupon code itself can currently be redeemed.

Examples:

  • 3 months free: discountType: 'percentage', discountValue: 100, benefitDurationUnit: 'months', benefitDurationValue: 3.
  • 20% off for 6 months: discountType: 'percentage', discountValue: 20, benefitDurationUnit: 'months', benefitDurationValue: 6.

Validation Rules ​

When a coupon is validated, the following checks are performed in order:

  1. Exists and active — Coupon must exist and have isActive: true
  2. Not before valid date — validFrom must be in the past (if set)
  3. Not expired — validUntil must be in the future (if set)
  4. Redemption limit — currentRedemptions must be less than maxRedemptions (if set)
  5. Plan applicability — If applicablePlanIds is non-empty, the target plan must be listed

Discount Calculation ​

  • Percentage: finalPrice = basePrice * (1 - discountValue / 100)
  • Fixed: finalPrice = max(0, basePrice - discountValue)

All amounts are rounded to two decimal places.

API Endpoints ​

Validate Coupon (Public) ​

POST /api/coupons/validate

Body:

json
{
  "code": "SAVE20",
  "planId": "plan-id"
}

Success Response:

Returns the coupon object if valid.

Error Responses:

CodeDescription
COUPON_NOT_FOUNDCoupon does not exist or is inactive
COUPON_NOT_YET_VALIDBefore validFrom date
COUPON_EXPIREDPast validUntil date
COUPON_MAX_REDEMPTIONSRedemption limit reached
COUPON_NOT_APPLICABLENot valid for the specified plan

Admin: CRUD ​

GET    /api/coupons
GET    /api/coupons/:couponId
POST   /api/coupons
PUT    /api/coupons/:couponId
DELETE /api/coupons/:couponId

SDK Usage ​

Public SDK ​

typescript
// Validate a coupon before checkout
try {
  const coupon = await sdk.payments.validateCoupon('SAVE20', 'plan-id');
  console.log(`${coupon.discountValue}% off!`);
} catch (err) {
  console.error('Invalid coupon:', err.message);
}

Admin SDK ​

typescript
// Create a 20% discount coupon
const coupon = await adminSdk.managePayments.createCoupon({
  code: 'LAUNCH20',
  discountType: 'percentage',
  discountValue: 20,
  isActive: true,
  applicablePlanIds: [],
});

// Create a limited-use fixed discount
await adminSdk.managePayments.createCoupon({
  code: 'FLAT10',
  discountType: 'fixed',
  discountValue: 10,
  currency: 'USD',
  maxRedemptions: 100,
  validUntil: new Date('2026-12-31'),
  isActive: true,
  applicablePlanIds: ['plan-pro'],
});

// First 3 months free, then standard pricing
await adminSdk.managePayments.createCoupon({
  code: 'FIRST3FREE',
  discountType: 'percentage',
  discountValue: 100,
  benefitDurationUnit: 'months',
  benefitDurationValue: 3,
  isActive: true,
  applicablePlanIds: [],
});

// Deactivate a coupon
await adminSdk.managePayments.deleteCoupon('coupon-id');

Build SaaS Products Without Limits.