Documentation

Commissions

Accrual rules, hold windows, and the commission state machine.

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 statusMeaningCommission commissionStatus
pendingClick recorded, no order yetpending (amount = 0)
commission_heldOrder paid, in the hold windowheld
qualifiedOrder paid, hold window passed (or zero)pending then credited
commission_paidPartner balance has been paid outpaid
rejectedFailed fraud check or admin-rejectedpending (amount unchanged for audit)
reversedOrder refunded → commission clawed backreversed
expiredClick never converted in the attribution windown/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:

POST/affiliate/commissions/process-heldJWT
const 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. 1

    Compute the amount

    Using calculateCommission — applies overrides (per-affiliate, per-product) then the program rule (flat, percentage, tiered).

  2. 2

    Pick the initial state

    commission_held if holdDays > 0, else qualified with immediate credit.

  3. 3

    Write the referral

    With commissionAmount, commissionType, commissionRate, commissionStatus.

  4. 4

    If immediate credit, push to wallet

    affiliate.data.stats.balance increments. The wallet record is updated through the Finance module.

  5. 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?

PUT/affiliate/affiliates/:id/commission-overrideJWT
await 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:

ActionEffect
approveForce-credit a held or qualified referral
rejectMark as rejected; if previously credited, clawback fires
holdMove from qualified back to commission_held (e.g. fraud review)
reverseRefund happened — clawback the commission from the partner balance
restoreUndo a reverse — re-credit
expireManually expire a stale pending referral
recalculateRe-run calculateCommission (e.g. after a program rate change)
override-amountReplace 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

GET/affiliate/referrals/audit/reportJWT
POST/affiliate/referrals/expire-staleJWT

The 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.