Production-ready coupon and referral system plugin for Payload CMS Ecommerce Plugin
npm install @wtree/payload-ecommerce-coupon


Production-ready coupon and referral system plugin for Payload CMS with seamless integration to the @payloadcms/plugin-ecommerce package.
- Coupon Mode (enableReferrals: false) β Traditional discount codes
- Referral Mode (enableReferrals: true) β Partner commissions + customer discounts
- Hybrid Mode (enableReferrals: true + referralConfig.allowBothSystems: true) β Both systems active
- β
Flexible Discounts β Percentage or fixed amount discounts (all amounts rounded to 2 decimals)
- β
Usage Controls β Global usage limit; usage is counted when an order is placed (not on apply)
- β
Per-customer limit β Optional limit per customer (requires customerEmail when applying)
- β
Conditions β Minimum/maximum order values (top-level fields), product restrictions
- β
Auto-Application β Seamless cart integration; cart total is reduced when a code is applied
- β
Commission Rules β Required. At least one rule per program. Each rule supports:
- Direct Basis: Separate Reward (Partner) and Referee Reward (Customer).
- Shared Basis: Define a total "pot" (e.g., 20% of order) and split it (e.g., 50/50) between partner and customer.
- β
Referrer/Referee inside each rule β Partner gets commission, customer gets discount; type (percentage/fixed), value, and optional max cap per rule.
- β
Partner Tracking β Commission earnings and referral performance (credited when order is placed)
- β
Auto-Generated Codes β Unique referral codes for each partner
- β
Partner Dashboard β Ready-to-use React components for partner stats
- β
Single Code Per Cart β Enforce one code (coupon or referral) per order
- β
REST API β Validate, apply, and record usage when order is placed
- β
Frontend Hooks β useCouponCode(), usePartnerStats(), validateCouponCode() for React/Next.js
- β
Auto-Integration β Extends carts/orders automatically
- β
Usage on Order β Coupon/referral usage and partner earnings are recorded when an order is placed (not when code is applied)
- β
Cart total helper β getCartTotalWithDiscounts(cart) for host app cart hooks so totals respect discounts
- β
Automatic Cart Recalculation β Hook ensures commissions and discounts are recalculated whenever cart items change
- β
Type-Safe β Full TypeScript support
- β
Access Control β Role-based permissions with partner role support
- β
Custom Admin Groups β Separate "Coupons" and "Referrals" categories
- β
Production-Ready β Comprehensive testing and error handling
``bash`
npm install @wtree/payload-ecommerce-coupon
- payload@^3.0.0 (Payload CMS)@payloadcms/plugin-ecommerce@>=3.0.0
- (required peer dependency)node@>=18.0.0
-
In your payload.config.ts:
`typescript
import { buildConfig } from 'payload'
import { ecommercePlugin } from '@payloadcms/plugin-ecommerce'
import { payloadEcommerceCoupon } from '@wtree/payload-ecommerce-coupon'
export default buildConfig({
plugins: [
ecommercePlugin({
// your ecommerce configuration
}),
payloadEcommerceCoupon({
enabled: true,
enableReferrals: true, // Enable referral system
defaultCurrency: 'USD',
// Referral-specific configuration
referralConfig: {
allowBothSystems: false, // Set true to allow both coupons and referrals
singleCodePerCart: true, // Only one code per order
defaultPartnerSplit: 70, // 70% to partner
defaultCustomerSplit: 30, // 30% discount to customer
},
// Custom admin panel groups
adminGroups: {
couponsGroup: 'Coupons',
referralsGroup: 'Referrals',
},
// Partner dashboard configuration
partnerDashboard: {
enabled: true,
showEarningsSummary: true,
showReferralPerformance: true,
showRecentReferrals: true,
showCommissionBreakdown: true,
},
// Access control
access: {
canUseCoupons: () => true,
canUseReferrals: () => true,
isAdmin: ({ req }) => req.user?.role === 'admin',
isPartner: ({ req }) => req.user?.role === 'partner',
},
// Optional: for per-customer coupon limit (defaults shown)
// orderIntegration: {
// ordersSlug: 'orders',
// orderCustomerEmailField: 'customerEmail',
// orderPaymentStatusField: 'paymentStatus',
// orderPaidStatusValue: 'paid',
// },
}),
],
})
`
After adding the plugin, run your Payload migration to create the new collections:
`bash`
npm run payload migrate
This will create collections for:
- Coupons β Manage discount codes (in "Coupons" group)
- Referral Programs β Set up partner commission structures (in "Referrals" group)
- Referral Codes β Track generated referral links (in "Referrals" group)
To enable the partner dashboard and role-based access, add a role field to your Users collection:
`typescript
// collections/Users.ts
import type { CollectionConfig } from 'payload'
export const Users: CollectionConfig = {
slug: 'users',
auth: true,
fields: [
{
name: 'role',
type: 'select',
options: [
{ label: 'Admin', value: 'admin' },
{ label: 'Partner', value: 'partner' },
{ label: 'Customer', value: 'customer' },
],
defaultValue: 'customer',
required: true,
},
// Or use multiple roles
{
name: 'roles',
type: 'select',
hasMany: true,
options: [
{ label: 'Admin', value: 'admin' },
{ label: 'Partner', value: 'partner' },
{ label: 'Customer', value: 'customer' },
],
defaultValue: ['customer'],
},
],
}
`
Coupon and referral usage is not counted when a code is applied to the cart. It is counted only when an order is placed successfully (e.g. paid). You must call the plugin when converting cart to order:
Option A β Call the API (e.g. from your Orders collection afterChange hook when paymentStatus === 'paid'):
`bash`
POST /api/coupons/record-order-usage
Content-Type: application/json
{ "orderId": "your-order-id" }
Option B β Use the server utility (in your Payload config or Orders hook):
`typescript
import { recordCouponUsageForOrder } from '@wtree/payload-ecommerce-coupon'
// In your Orders collection afterChange hook, when order is paid/completed:
if (doc.paymentStatus === 'paid' && (doc.appliedCoupon || doc.appliedReferralCode)) {
await recordCouponUsageForOrder(payload, doc, pluginConfig)
}
`
- Coupon: increments the couponβs usageCount.usageCount
- Referral: increments the referral codeβs and successfulReferralsCount, and adds order.partnerCommission to the referral codeβs totalEarnings and pendingEarnings (referrer gets commission; referee discount is already on the order).
Usage rule
- A customer can use a coupon until the couponβs global usage limit or expiry date (usage is counted when the order is placed, not when the code is applied).
- Optional per-customer limit: If you set Per customer limit on a coupon, the customer must provide their email when applying (e.g. customerEmail in the apply request). The coupon is rejected once they have that many paid orders with that coupon. You can pass customerEmail when validating so the UI can show βlimit reachedβ before apply.
Monetary values
- All discount, commission, and total values are rounded to 2 decimal places.
Cart total in your app
- The plugin writes the reduced total when a code is applied. If your host app recalculates the cart total (e.g. in a beforeChange hook when items change), use the formula total = subtotal β discountAmount β customerDiscount so the discount is not overwritten. Use the provided helper in your Carts collection:
`typescript
import { getCartTotalWithDiscounts } from '@wtree/payload-ecommerce-coupon'
// In your Carts collection beforeChange hook, after setting items/subtotal:
data.total = getCartTotalWithDiscounts(data)
`
- Optional config for per-customer limit: orderIntegration with ordersSlug, orderCustomerEmailField, orderPaymentStatusField, orderPaidStatusValue (defaults: 'orders', 'customerEmail', 'paymentStatus', 'paid').
Server utilities (for host app)
- getCartTotalWithDiscounts(cart) β Returns roundTo2(subtotal - discountAmount - customerDiscount). Use in your Carts beforeChange (or wherever you compute total) so the displayed total always reflects coupon/referral discounts.recordCouponUsageForOrder(payload, order, pluginConfig)
- β Call when an order is paid to increment coupon/referral usage and credit partner earnings (see step 4 above).
#### Apply Coupon/Referral Code
`typescript
import { useCouponCode } from '@wtree/payload-ecommerce-coupon'
function CheckoutComponent() {
const [code, setCode] = useState('')
const [cartId, setCartId] = useState('your-cart-id')
const applyCode = async () => {
const result = await useCouponCode({
code,
cartID: cartId,
// When a coupon has per-customer limit, pass customerEmail so the limit can be enforced
// customerEmail: customerEmailFromAuthOrForm,
})
if (result.success) {
if (result.coupon) {
console.log('Coupon applied! Discount:', result.discount)
} else if (result.referralCode) {
console.log('Referral applied!')
console.log('Your discount:', result.customerDiscount)
console.log('Partner commission:', result.partnerCommission)
}
} else {
console.error('Error:', result.error)
}
}
return (
#### Partner Dashboard
Use the
usePartnerStats hook to build a custom dashboard, or use the pre-built dashboard components when available from the package. See Partner Dashboard docs.`typescript
import { usePartnerStats } from '@wtree/payload-ecommerce-coupon'// Build custom dashboard with the hook
function CustomPartnerDashboard() {
const [data, setData] = useState(null)
useEffect(() => {
const fetchStats = async () => {
const result = await usePartnerStats()
if (result.success) {
setData(result.data)
}
}
fetchStats()
}, [])
if (!data) return
Loading... return (
Your Earnings
Total: ${data.stats.totalEarnings}
Pending: ${data.stats.pendingEarnings}
Paid: ${data.stats.paidEarnings}
Your Referral Codes
{data.referralCodes.map(code => (
{code.code}
Uses: {code.usageCount}
))}
)
}
`π¨βπΌ Admin Usage Guide
$3
#### Coupon Mode (
enableReferrals: false)Best for traditional discount campaigns, seasonal sales, and customer loyalty programs.
#### Referral Mode (
enableReferrals: true)Best for affiliate marketing, partner programs, and customer acquisition through referrals.
#### Hybrid Mode (
enableReferrals: true + allowBothSystems: true)Best when you need both traditional coupons AND partner referrals, but want to enforce only one code per order.
$3
1. Navigate to Admin Panel β Go to "Coupons" collection (under "Coupons" group)
2. Create New Coupon:
- Code:
WELCOME10 (unique identifier)
- Type: Percentage or Fixed Amount
- Value: 10 (10% or $10)
- Description: "Welcome discount for new customers"
- Active From/Until: Set validity period
- Usage Limit: Maximum uses (optional)
- Per Customer Limit: Uses per customer (optional)
- Min/Max Order Value: Order value constraints$3
1. Navigate to Admin Panel β Go to "Referral Programs" (under "Referrals" group)
2. Create Referral Program with Commission Rules (required β at least one). Each rule has:
- Name: e.g. "Default" or "Electronics"
- Applies To: All Products, Specific Categories, or Specific Products
- Referrer Reward (inside the rule): Commission for the partner. Type: Percentage of Order or Fixed Amount. Value: e.g. 65 = 65% of order value. Optional Max Reward.
- Referee Reward (inside the rule): Discount for the customer. Type: Percentage Discount or Fixed Amount. Value: e.g. 30 = 30% off. Optional Max Reward.
There are no outer Referrer Reward / Referee Reward fields β only Commission Rules, and each rule contains its own Referrer Reward and Referee Reward.
$3
- Referrer (partner) receives commission β credited to the referral codeβs
totalEarnings and pendingEarnings when the order is placed (via record-order-usage).
- Referee (customer) receives a discount β applied to the order; stored on cart/order as customerDiscount.#### Example: Commission Rules with Split
- Order Total: $100 (Electronics category)
- Total Commission: 15% = $15
- Partner Share: 70% of $15 = $10.50 (commission to referrer)
- Customer Discount: 30% of $15 = $4.50 (discount to referee)
$3
1. Create Partner Account: Set user role to "partner"
2. Generate Referral Code: Partners can create codes in "Referral Codes" collection
3. Track Performance: View usage count, earnings, and successful referrals
4. Payout Management: Track pending vs paid earnings
π REST API Endpoints
$3
#### POST /api/coupons/validate
Validate a code without applying it. Optionally pass
customerEmail to check per-customer limit for coupons that have one.`bash
curl -X POST http://localhost:3000/api/coupons/validate \
-H "Content-Type: application/json" \
-d '{"code": "WELCOME10", "cartValue": 5000}'With per-customer limit check:
-d '{"code": "WELCOME10", "cartValue": 5000, "customerEmail": "user@example.com"}'
`#### POST /api/coupons/apply
Apply a code to a cart. Does not increment usage; usage is recorded when you call the record-order-usage endpoint for a placed order. For coupons with per-customer limit, include
customerEmail so the limit can be enforced.`bash
curl -X POST http://localhost:3000/api/coupons/apply \
-H "Content-Type: application/json" \
-d '{"code": "WELCOME10", "cartID": "cart-123"}'With per-customer limit (required when coupon has per-customer limit):
-d '{"code": "WELCOME10", "cartID": "cart-123", "customerEmail": "user@example.com"}'
`#### POST /api/coupons/record-order-usage
Record coupon and referral usage for a successfully placed order. Call this once per order when the order is paid/completed (e.g. from your Orders
afterChange hook).Request body:
{ "orderId": "string" }`bash
curl -X POST http://localhost:3000/api/coupons/record-order-usage \
-H "Content-Type: application/json" \
-d '{"orderId": "order-123"}'
`Response:
{ "success": true, "recordedCoupon": boolean, "recordedReferral": boolean }$3
#### GET /api/referrals/partner-stats
Get partner dashboard data (requires authentication).
`bash
curl -X GET http://localhost:3000/api/referrals/partner-stats \
-H "Cookie: payload-token=your-auth-token"
`Response:
`json
{
"success": true,
"data": {
"stats": {
"totalEarnings": 1250.50,
"pendingEarnings": 350.00,
"paidEarnings": 900.50,
"totalReferrals": 45,
"successfulReferrals": 38,
"conversionRate": 84.44,
"recentReferrals": [...],
"monthlyEarnings": [...]
},
"referralCodes": [...],
"program": {
"name": "Partner Program",
"commissionRate": 10,
"customerDiscount": 5
}
},
"currency": "USD"
}
`βοΈ Configuration
$3
`typescript
export type CouponPluginOptions = {
enabled?: boolean // Enable/disable the plugin (default: true)
enableReferrals?: boolean // Enable referral system (default: false)
allowStackWithOtherCoupons?: boolean // Allow multiple coupons (default: false)
defaultCurrency?: string // Currency code (default: 'USD')
autoIntegrate?: boolean // Auto-extend carts/orders (default: true) collections?: {
couponsSlug?: string // Default: 'coupons'
referralProgramsSlug?: string // Default: 'referral-programs'
referralCodesSlug?: string // Default: 'referral-codes'
/* Override the default coupons collection configuration /
couponsCollectionOverride?: (params: { defaultCollection: any }) => any | Promise
/* Override the default referral programs collection configuration /
referralProgramsCollectionOverride?: (params: { defaultCollection: any }) => any | Promise
/* Override the default referral codes collection configuration /
referralCodesCollectionOverride?: (params: { defaultCollection: any }) => any | Promise
}
endpoints?: {
applyCoupon?: string // Default: '/coupons/apply'
validateCoupon?: string // Default: '/coupons/validate'
partnerStats?: string // Default: '/referrals/partner-stats'
recordOrderUsage?: string // Default: '/coupons/record-order-usage'
}
access?: {
canUseCoupons?: Access // Who can use coupons
canUseReferrals?: Access // Who can use referrals
isAdmin?: Access // Who can manage codes/programs
isPartner?: Access // Who has partner access
}
referralConfig?: {
allowBothSystems?: boolean // Allow coupons + referrals (default: false)
singleCodePerCart?: boolean // One code per order (default: true)
defaultPartnerSplit?: number // Default partner % (default: 70)
defaultCustomerSplit?: number // Default customer % (default: 30)
}
adminGroups?: {
couponsGroup?: string // Admin group for coupons (default: 'Coupons')
referralsGroup?: string // Admin group for referrals (default: 'Referrals')
}
partnerDashboard?: {
enabled?: boolean // Enable dashboard (default: true)
showEarningsSummary?: boolean // Show earnings widget (default: true)
showReferralPerformance?: boolean // Show performance widget (default: true)
showRecentReferrals?: boolean // Show recent referrals (default: true)
showCommissionBreakdown?: boolean // Show breakdown (default: true)
}
/* Optional: for per-customer coupon limit (query paid orders by customer) /
orderIntegration?: {
ordersSlug?: string // Default: 'orders'
orderCustomerEmailField?: string // Default: 'customerEmail'
orderPaymentStatusField?: string // Default: 'paymentStatus'
orderPaidStatusValue?: string // Default: 'paid'
}
}
`$3
You can override the default collection configurations to customize fields, hooks, or other collection settings. This allows you to extend or modify the plugin's behavior without forking the code.
`typescript
payloadEcommerceCoupon({
collections: {
// Override coupons collection
couponsCollectionOverride: async ({ defaultCollection }) => {
return {
...defaultCollection,
fields: [
...defaultCollection.fields,
// Add custom field to coupons
{
name: 'customField',
type: 'text',
label: 'Custom Field',
},
],
hooks: {
...defaultCollection.hooks,
// Add custom hook
beforeChange: [
...(defaultCollection.hooks?.beforeChange || []),
async ({ data, req, operation }) => {
// Custom beforeChange logic
return data
},
],
},
}
}, // Override referral programs collection
referralProgramsCollectionOverride: ({ defaultCollection }) => {
return {
...defaultCollection,
admin: {
...defaultCollection.admin,
defaultColumns: ['name', 'isActive', 'totalReferrals'],
},
}
},
// Override referral codes collection
referralCodesCollectionOverride: async ({ defaultCollection }) => {
return {
...defaultCollection,
fields: [
...defaultCollection.fields,
{
name: 'customCodeField',
type: 'select',
label: 'Custom Code Type',
options: ['standard', 'premium'],
defaultValue: 'standard',
},
],
}
},
},
})
`$3
`typescript
payloadEcommerceCoupon({
access: {
// Anyone can use coupons
canUseCoupons: () => true, // Only authenticated users can use referrals
canUseReferrals: ({ req }) => Boolean(req.user),
// Only admins can manage
isAdmin: ({ req }) => req.user?.role === 'admin',
// Partner role check (supports both single role and array)
isPartner: ({ req }) => {
const user = req.user
if (!user) return false
if (user.role === 'partner') return true
if (Array.isArray(user.roles) && user.roles.includes('partner')) return true
return false
},
},
})
`π¦ API Reference
$3
`typescript
import {
payloadEcommerceCoupon, // Collection creation functions
createCouponsCollection,
createReferralCodesCollection,
createReferralProgramsCollection,
// Frontend hooks
useCouponCode,
validateCouponCode,
usePartnerStats,
// Server-only: record usage when order is placed
recordCouponUsageForOrder,
} from '@wtree/payload-ecommerce-coupon'
`Dashboard components (
PartnerDashboard, EarningsSummary, ReferralPerformance, RecentReferrals, ReferralCodes) are available from the package source; see Partner Dashboard docs for usage.$3
You can use the collection creation functions directly in your Payload config to customize collections before they're added to the config.
`typescript
import { buildConfig } from 'payload'
import { ecommercePlugin } from '@payloadcms/plugin-ecommerce'
import { payloadEcommerceCoupon, createCouponsCollection } from '@wtree/payload-ecommerce-coupon'export default buildConfig({
plugins: [
ecommercePlugin({
// your ecommerce configuration
}),
payloadEcommerceCoupon({
// plugin configuration
}),
],
collections: [
// The plugin adds collections automatically; use overrides in plugin config to customize
],
})
`π¨ Partner Dashboard
The plugin provides hooks and (when using the source) React components for partner dashboards. Use
usePartnerStats() to fetch stats; for pre-built dashboard components and styling, see Partner Dashboard documentation.π§ Troubleshooting
$3
#### "A code has already been applied to this cart"
This occurs when
singleCodePerCart: true and a code is already applied.- Solution: Remove the existing code before applying a new one, or set
singleCodePerCart: false#### Partner can't see their referral codes
- Ensure the user has
role: 'partner' or roles: ['partner']
- Check the isPartner access control function#### Usage count or partner earnings not updating
- Usage is not incremented when a code is applied to the cart. Call record-order-usage (or
recordCouponUsageForOrder) when an order is placed/paid. See Record usage when order is placed.#### Commission not calculating correctly
- Ensure at least one Commission Rule exists and each rule has Referrer Reward and Referee Reward set
- Verify cart has valid
subtotal or total and items match ruleβs appliesTo (all / categories / products)π Future Features (Roadmap)
The following features are planned for future releases:
| Feature | Status | Description |
| ---------------------- | ---------- | ---------------------------------------------------------------- |
| Multi-tier commissions | π Planned | Support for tiered commission rates based on performance |
| Automatic payouts | π Planned | Integration with payment providers for automatic partner payouts |
| Referral analytics | π Planned | Advanced analytics and reporting dashboard |
| Email notifications | π Planned | Automated emails for referral events |
| Custom code generation | π Planned | Allow partners to create custom branded codes |
| Fraud detection | π Planned | Automatic detection of suspicious referral patterns |
| Bulk code import | π Planned | Import coupons/codes from CSV |
| A/B testing | π Planned | Test different commission structures |
$3
| Feature | This Plugin | ReferralCandy | Refersion | Custom Build |
| ----------------------- | ----------- | ------------- | --------- | ------------ |
| Payload CMS Integration | β
Native | β | β | β οΈ Manual |
| Coupon System | β
| β | β | β οΈ Manual |
| Referral System | β
| β
| β
| β οΈ Manual |
| Partner Dashboard | β
| β
| β
| β οΈ Manual |
| Commission Rules | β
| β οΈ Limited | β
| β οΈ Manual |
| Single Code Enforcement | β
| β | β | β οΈ Manual |
| TypeScript Support | β
| β | β | β οΈ Varies |
| Self-Hosted | β
| β | β | β
|
| Monthly Cost | Free | $49+ | $89+ | Dev Time |
π§ͺ Testing
`bash
Run all tests
npm testWatch mode
npm run test:watchCoverage report
npm run test:coverage
``- API Reference
- Compatibility Matrix
- Contributing Guide
- GitHub: https://github.com/technewwings/payload-ecommerce-coupon
- NPM: https://npmjs.com/package/@wtree/payload-ecommerce-coupon
- Payload CMS: https://payloadcms.com
- Payload Dashboard Docs: https://payloadcms.com/docs/custom-components/dashboard
MIT License Β© 2026 wtree. See LICENSE for details.
Contributions are welcome! See CONTRIBUTING.md for guidelines.