Promo Codes
The promo code system supports percentage and fixed-amount discounts, Stripe coupon synchronization, token-based invitations, and reseller bulk purchases.
Discount Types
| Type | Behavior | Example |
|---|---|---|
PERCENTAGE | Reduces price by percentage | SAVE20 gives 20% off |
FIXED_AMOUNT | Reduces price by flat amount (currency-specific) | TEN_OFF gives $10 off |
Validation Checks
When a user redeems a code, the backend runs 8 sequential checks before applying the discount:
| # | Check | Error on failure |
|---|---|---|
| 1 | Code exists | not found |
| 2 | Code is active (isActive: true) | deactivated |
| 3 | Code not expired (expiresAt > now) | expired |
| 4 | Usage limit not reached (usageCount < usageLimit) | limit reached |
| 5 | User has not already redeemed | already redeemed |
| 6 | Code applies to selected plan (applicablePlanIds) | not applicable |
| 7 | User is in allowed list (restrictedToUserIds / restrictedToCompanyIds) | not authorized |
| 8 | User holds invitation if requiresInvitation: true | invitation required |
Stripe Coupon Integration
When a promo code is created, the backend creates a matching Stripe coupon. On redemption, the coupon is applied to the Stripe checkout session so the discount appears on the invoice.
Invitation System
Codes with requiresInvitation: true can only be redeemed by invited users.
| Field | Description |
|---|---|
invitedEmail | Recipient email address |
message | Optional personal message |
token | Auto-generated UUID for acceptance link |
expiresAt | 7-day expiry from creation |
status | PENDING / ACCEPTED / EXPIRED / DECLINED |
The acceptPromoCodeInvitation mutation is public (no JWT required) so unauthenticated recipients can accept via email link.
Reseller / Bulk Purchases
Codes with isPersonal: true restrict usage to the owner. Combined with a high usageLimit, this supports reseller scenarios where a partner buys in bulk and distributes via invitations.
// Reseller code example
{
code: "PARTNER_2025",
discountType: "PERCENTAGE",
discountValue: 30,
isPersonal: true,
requiresInvitation: true,
usageLimit: 500,
campaignName: "Q1 Reseller Program"
}
Computed Fields
These fields are resolved automatically on every PromoCode query:
| Field | Type | Description |
|---|---|---|
remainingUses | Int | usageLimit - usageCount (null if unlimited) |
isExpired | Boolean | expiresAt < now |
canBeUsed | Boolean | Active, not expired, uses remaining |
GraphQL Operations
Mutations
| Mutation | Auth | Purpose |
|---|---|---|
createPromoCode(input) | JWT | Create a new code with discount config |
redeemPromoCode(input) | JWT | Apply code at checkout (code + planId) |
deactivatePromoCode(id) | JWT | Set isActive: false |
inviteToPromoCode(input) | JWT | Send email invitation to redeem |
acceptPromoCodeInvitation(token) | Public | Accept invitation via token |
Queries
| Query | Auth | Purpose |
|---|---|---|
myPromoCodes | JWT | List codes owned by current user |
promoCode(id) | JWT | Single code with details |
myPromoCodeStatistics | JWT | Usage and revenue stats |
mySentPromoCodeInvitations | JWT | Outbound invitations |
myReceivedPromoCodeInvitations | JWT | Inbound invitations |
Common Patterns
// Percentage discount with usage cap
{ code: "SAVE20", discountType: "PERCENTAGE", discountValue: 20, usageLimit: 100 }
// Fixed discount with currency
{ code: "TEN_OFF", discountType: "FIXED_AMOUNT", discountValue: 10, currency: "usd", usageLimit: 50 }
// Invitation-only exclusive code
{ code: "EXCLUSIVE", discountType: "PERCENTAGE", discountValue: 30, requiresInvitation: true, isPersonal: true }
// Time-limited flash sale
{ code: "FLASH50", discountType: "PERCENTAGE", discountValue: 50, expiresAt: "2026-12-31T23:59:59Z", usageLimit: 10 }
Deactivating a code (deactivatePromoCode) does not revoke already-applied Stripe coupons. Active subscriptions with the discount continue until renewal.