Skip to content

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 a held commission 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 for invoice.paid, not invoice.payment_succeeded. Make sure the endpoint is subscribed to invoice.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 (with PLATFORM_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:

  1. They self-serve. Any user can open Settings → Referrals and click Generate Referral Code. That creates their SalesReferrer record at the default 7% rate. They then appear in your roster.
  2. 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):

  1. /admin/sales-referrers → find the referrer → Edit.
  2. Set Commission % (and optionally the Attribution window in days — default 60).
  3. 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:

  1. /admin/sales-referrers → the referrer → Assign customer.
  2. Enter the organization and pick a start date (effective-from).
  3. 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; releasing is counted together with held, and failed is a payout that will be retried).
  • voided commissions (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 out referralPayoutHoldDays (30).
  • releasing — the hourly release_referral_payouts job has atomically claimed it and is transferring to the referrer's Stripe account (idempotency-keyed so a retry never double-pays).
  • paid — transfer succeeded (stripeTransferId recorded).
  • 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_enabled false). Commissions stay held until 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 a Referral attribution.
  • Payouts not releasing — confirm the release_referral_payouts recurring job is scheduled on the worker and that referralPayoutHoldDays has elapsed.

Next: Sales Referrals — Deploy Runbook