Commission is the money that gets owed when a referral converts. Each referral record carries a status that drives whether the partner sees the amount as expected, held, credited, or paid. This page explains the lifecycle and the operations available on it.
Commission states
A referral has two related fields:
data.status— the referral lifecycle (covers click → conversion → payment → reversal)data.commissionStatus— the commission-payment lifecycle (pending,held,credited,paid,reversed)
The states you'll see most often:
Referral status | Meaning | Commission commissionStatus |
|---|---|---|
pending | Click recorded, no order yet | pending (amount = 0) |
commission_held | Order paid, in the hold window | held |
qualified | Order paid, hold window passed (or zero) | pending then credited |
commission_paid | Partner balance has been paid out | paid |
rejected | Failed fraud check or admin-rejected | pending (amount unchanged for audit) |
reversed | Order refunded → commission clawed back | reversed |
expired | Click never converted in the attribution window | n/a |
Hold window
The program's payout.holdDays is a buffer between conversion and crediting. It exists so a refund doesn't trigger a clawback on already-paid commission. Tune it to your refund policy:
- E-commerce with frequent returns → 14–30 days
- Digital/subscription with no refunds → 0 days (immediate credit)
- High-fraud verticals → 60+ days
When holdDays elapses, an admin can run the sweep manually:
/affiliate/commissions/process-heldJWTconst res = await fetch('/affiliate/commissions/process-held', {
method: 'POST',
headers: { orgid: 'my-org', Authorization: `Bearer ${jwt}` },
}).then(r => r.json());
// { processed: 23, totalCredited: 1240.50 }
This finds every commission_held referral past its hold and credits the affiliate wallet. You'd typically schedule this via the Automation module on a daily cron rather than running it by hand.
Accrual
When attributeOrder runs successfully, it does:
- 1
Compute the amount
Using
calculateCommission— applies overrides (per-affiliate, per-product) then the program rule (flat,percentage,tiered). - 2
Pick the initial state
commission_heldifholdDays > 0, elsequalifiedwith immediate credit. - 3
Write the referral
With
commissionAmount,commissionType,commissionRate,commissionStatus. - 4
If immediate credit, push to wallet
affiliate.data.stats.balanceincrements. The wallet record is updated through the Finance module. - 5
Update aggregate stats
On both the affiliate (
stats.totalCommission,stats.pendingCommission) and program records (stats.totalConversions,stats.totalCommissionPaid).
Per-affiliate override
VIP partner with a custom rate?
/affiliate/affiliates/:id/commission-overrideJWTawait fetch(`/affiliate/affiliates/${affiliateId}/commission-override`, {
method: 'PUT',
headers: {
orgid: 'my-org',
Authorization: `Bearer ${jwt}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ type: 'percentage', value: 25 }),
});
The override applies to all future referrals for that affiliate; existing referrals are untouched. To reset to the program rate, set the override to null via direct data API.
Per-product override
A product can carry an affiliateCommission field that takes precedence over the program rate when calculating per-item commission. Set affiliateEligible: false on items that should pay zero (e.g. cost-price loss-leaders).
Manual transitions
The transition endpoint moves a referral by action name:
| Action | Effect |
|---|---|
approve | Force-credit a held or qualified referral |
reject | Mark as rejected; if previously credited, clawback fires |
hold | Move from qualified back to commission_held (e.g. fraud review) |
reverse | Refund happened — clawback the commission from the partner balance |
restore | Undo a reverse — re-credit |
expire | Manually expire a stale pending referral |
recalculate | Re-run calculateCommission (e.g. after a program rate change) |
override-amount | Replace commissionAmount with an explicit value |
await fetch(`/affiliate/referrals/${id}/transition`, {
method: 'POST',
headers: { orgid: 'my-org', Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'override-amount',
amount: 75,
reason: 'Negotiated bonus',
actor: '[email protected]',
}),
});
Every transition writes an audit entry to the referral's history array — actor, action, reason, timestamp, before/after amounts.
Clawback
When a referred order is refunded, the Storefront emits an event the affiliate module listens to. If the corresponding referral is qualified or commission_paid, the system runs the reverse action — debits the partner balance and writes the audit entry. If the partner has already withdrawn the credit (already paid out), the reverse is recorded as a negative balance entry that's netted off the next payout.
Audit and stale cleanup
/affiliate/referrals/audit/reportJWT/affiliate/referrals/expire-staleJWTThe audit endpoint surfaces stuck referrals (e.g. commission_held past their hold but never processed), zero-commission conversions, and orphaned referrals (affiliate or program no longer exists). Use it as the basis of a periodic data-quality check.
expire-stale cleans up pending (click-only) referrals past their attribution window. Run it daily.
Commission state changes never delete the referral record — they always append to history. Even rejected and reversed referrals are kept indefinitely for finance reconciliation.