Appearance
Sales Referrals — Admin Setup
How platform staff run the sales-referral program: prerequisites, adding a salesperson, setting rates, manual attribution, and reading payouts. This is the console guide; for the one-time deploy/migration steps see Sales Referrals — Deploy Runbook.
The admin console lives at /admin/sales-referrers (Platform nav → Sales Referrers). It's gated by PLATFORM_ADMIN_USER_IDS like the rest of /admin/* — org role is irrelevant.
Prerequisites (must be true before the program works)
Stripe Connect (Express) enabled on the platform Stripe account. Referral payouts are transfers to each referrer's own Express account.
Stripe webhook events subscribed. The commission engine reacts to these — if they aren't subscribed, commissions never get created, paused, or voided:
invoice.paid— creates aheldcommission for the attributed referrer.invoice.payment_failed— pauses the referral.customer.subscription.updated— syncs the referral's active/paused state from the subscription's billing status.customer.subscription.deleted— ends the referral.charge.refunded— voids (full) or recomputes (partial) the commission.charge.dispute.created— voids the commission.
Note on
invoice.paid: the engine listens forinvoice.paid, notinvoice.payment_succeeded. Make sure the endpoint is subscribed toinvoice.paid. See the runbook for the exact add steps.PlatformConfig.referralPayoutHoldDays— the payout holding window in days. Defaults to 30. Set it in Platform Config if you want a different hold.PLATFORM_ADMIN_USER_IDS— the allowlist of Clerk user IDs who can reach this console (withPLATFORM_ADMIN_ENFORCE=true).The payout release job (
release_referral_payouts, hourly) must be running on the worker. It's registered with the other BullMQ recurring jobs; confirm it after deploy.
Add a salesperson
There are two ways a person becomes a referrer:
- They self-serve. Any user can open Settings → Referrals and click Generate Referral Code. That creates their
SalesReferrerrecord at the default 7% rate. They then appear in your roster. - They get attributed first. If you manually assign them to an org (below) before they've generated a code, their referrer record is created as part of that.
Either way, once they exist you can adjust their rate, attribution window, and status from the console.
Set or change a commission rate
Default is 7%. To give a partner a custom rate (e.g. 10% for an experienced rep with a large network):
/admin/sales-referrers→ find the referrer → Edit.- Set Commission % (and optionally the Attribution window in days — default 60).
- Save.
The rate is read live at invoice time and snapshotted onto each commission, so:
- Changing a rate affects only invoices paid after the change.
- Already-recorded commissions keep the rate they were created with — history doesn't retroactively change.
Pause / reactivate a referrer
Set a referrer's status to inactive from the Edit dialog. While inactive:
- Their code can't be newly applied.
- No new commissions accrue.
- Existing attributed customers stay linked (so reactivating resumes earning).
This is distinct from a customer pausing — that's driven automatically by the customer's subscription billing state, not something you toggle here.
Manually assign a referrer to an org (the "signed up without the code" case)
When a salesperson referred a customer but the customer signed up through the normal flow (no ?ref link), attribute it by hand:
/admin/sales-referrers→ the referrer → Assign customer.- Enter the organization and pick a start date (effective-from).
- Save.
Commission then accrues on invoices paid on or after that start date. Rules enforced on assign:
- One referrer per org — if the org already has one, the assign is rejected.
- No self-referral — the referrer can't be a member of the org being attributed.
- The referrer must be active.
Read the dashboard (what the numbers mean)
The roster and each referrer's detail use canonical buckets derived from commission status:
- Earned / Paid out = commissions with status
paid. - Upcoming =
held+releasing+pending+failed(everything accrued but not yet paid;releasingis counted together withheld, andfailedis a payout that will be retried). voidedcommissions (refunded/disputed) are excluded from both.
The Commissions feed (filterable) lists individual commission rows with their invoice amount (excl. tax), rate applied, status, and hold date.
Payout lifecycle (hold → release / void)
Each commission moves through this state machine:
invoice.paid ──▶ held ──(30-day hold elapses & Stripe payable)──▶ releasing ──▶ paid
│ │
│ refund (full) / dispute └─(transfer fails)─▶ failed ──(retry)──▶ releasing
▼
voided refund (partial) ──▶ commission amount recomputed on the retained net- held — created on
invoice.paid; waits outreferralPayoutHoldDays(30). - releasing — the hourly
release_referral_payoutsjob has atomically claimed it and is transferring to the referrer's Stripe account (idempotency-keyed so a retry never double-pays). - paid — transfer succeeded (
stripeTransferIdrecorded). - failed — transfer errored; the job retries on a backoff.
- pending — transient waiting-to-retry state between attempts.
- voided — the invoice was fully refunded or disputed; no payout. A partial refund instead recomputes the commission on the retained amount (it does not void).
Which commissions a refund/dispute can still touch: the charge.refunded / charge.dispute.created webhook handlers only adjust commissions still in held, pending, or failed (i.e. not yet paid and not mid-transfer). A commission that's already paid is not clawed back. In addition, the release job re-checks the charge for refunds right before it transfers, so a refund that lands while a commission is releasing is still caught; a dispute that lands during that brief releasing window is not re-checked there.
A commission only pays out once the referrer's Stripe account is connected and payouts are enabled — otherwise it stays held until it is.
Troubleshooting
- A referrer shows "connected" but no payouts — Stripe may still have their Express account in review (
payouts_enabledfalse). Commissions stayhelduntil Stripe enables payouts. The referrer's own Settings → Referrals page surfaces this ("Connected, in review"). - Commissions not being created — verify
invoice.paid(and the other events above) are subscribed on the Stripe webhook endpoint, and that the org actually has aReferralattribution. - Payouts not releasing — confirm the
release_referral_payoutsrecurring job is scheduled on the worker and thatreferralPayoutHoldDayshas elapsed.