Facilitated payouts
Apex runs the money rail for partner commissions via Stripe Connect. Two critical properties:
- Apex never advances funds. Money only moves if the merchant has balance in their Stripe account. No platform overdraft.
- Money movement happens in the merchant's Stripe context. Transfers fire under
{stripeAccount: merchantStripeAccountId}— Apex's platform balance is never touched.
This makes Apex a facilitator, not an escrow. The tradeoff: the partner has to wait until the merchant's Stripe balance is there + the scheduled payout cycle hits.
The shape of a payout
merchant's Stripe balance
↓ transfer (principal, in merchant's account context)
partner's Stripe Express account
+
merchant's Stripe balance
↓ transfer (fee, in merchant's account context)
Apex Connect platform account
Two stripe.transfers.create calls per payout. Same idempotency batch (payout:<id>:principal + payout:<id>:fee). If the principal fires but the fee fails, the retry only re-attempts the fee leg.
The full lifecycle of one commission
- Conversion attribution — partner's link drives a signup; the contact is stamped with
referringAffiliateId - Purchase / subscription event — commission rules evaluate the event type (
install,purchase,subscription_renewal), auto-credit fires, creates anAffiliateConversionwithstatus: "pending"(or"pending_review"if risk score ≥ 0.5) - Merchant approves from the payout queue — transitions to
"approved", creates aHoldbackEntrysplitting commission into available + held per tier.autoPayoutPolicydoesn't approve sales; it only sets when approved money is sent. - Release cron fires past-due holdback entries (after 30 / 60 / 90 days by tier) — flips
releasedAt - Payout orchestrator runs per merchant's
autoPayoutPolicyschedule (daily / weekly / manual) — batches all released-available entries per partner, fires the pair of transfers - Held portion stays reserved — only releases + pays out after the hold window. Chargebacks in the interim debit this portion first.
Recurring subscription payouts
For subscription_renewal events, the flow repeats on every renewal cycle:
- Stripe fires
invoice.paidfor the renewal - Apex matches the subscription to the original referring partner via
referringAffiliateId - The
subscription_renewalcommission rule evaluates — creates a newAffiliateConversion - If
maxCreditsis set on the rule and the partner has already earned that many renewal commissions, the event is skipped - The new conversion enters the same approval → holdback → release → payout pipeline
Partners see recurring earnings accumulate in their portal with a clear breakdown per subscription and renewal period.
autoPayoutPolicy — merchant control of when transfers fire
| Policy | Trigger |
|---|---|
manual | Merchant presses Pay now in Organization settings → Payouts |
auto | Daily cron — every released-available balance over $0 pays out |
auto_under_cap | Every Friday, or on the 1st of each month with the Monthly schedule, in the merchant's time zone. A payout over autoPayoutCapUsd waits for Pay now |
Set it for the workspace, and optionally on a program. A program's own setting replaces the workspace's for that program's sales, so one program can pay daily while the rest pay weekly. Pay now still pays everything that has released.
Insufficient balance
When the merchant's Stripe balance is short, the executor returns insufficient_balance with the shortfall USD and a 24-hour retryAtMs. The cron schedules a retry. We don't pull funds from Apex.
A graduated, non-accusatory escalation kicks in:
| Day | Failures | Action |
|---|---|---|
| 0 | 1st | Silent — retry in 24h |
| 1 | 2nd (~48h) | Merchant email + internal notification |
| 2 | 3rd (~72h) | Dashboard card + 2nd email + partner visibility flips per trust tier |
| 7 | 7th | Open a support case (ruleId: merchant_cash_flow, severity: medium, not labeled as fraud). Retries pause until human review. |
Automation never freezes autoPayoutPolicy. A human (Fraud Ops or account manager) is always the one who pulls that trigger.
Partner-visible state during delays
Delay visibility is tiered by trust:
| Partner tier | Day 0-2 | Day 3-6 | Day 7+ |
|---|---|---|---|
| NEW | "Your payout is processing" | "Your payout is still processing" | "This payout is delayed — our team is working with the merchant" |
| TRUSTED | "Your payout is processing" | "Your merchant is topping up their Stripe balance" | "This payout is delayed — our team is working with the merchant" |
| VERIFIED | "Your payout is processing" | "Your merchant is topping up their Stripe balance" | "This payout is delayed — our team is working with the merchant" |
Partner-facing copy never uses the word "fraud". Merchant-facing emails use "cash flow" and "action needed" — never "fraud" — at every escalation level.
Fees
Apex charges a facilitation fee on every transfer: 1.5% + $0.50 by default. Enterprise merchants can negotiate custom rates via the Stripe Connect fees section on /admin/pricing. See Fee policy.
Fees are snapshotted on the HoldbackEntry at approval time. Admin edits after approval don't change what a merchant owes on already-approved commissions.
Tax compliance
Stripe collects each partner's tax details when they set up their Stripe Express account, before the first payout. Any year-end tax forms that apply to a partner are sent through Stripe and kept in their Stripe Express dashboard. You don't collect partner tax forms yourself. See Tax compliance for more.
Clawbacks
When a charge.refunded webhook lands, Apex:
- Finds every conversion linked to the
paymentIntent - Flips them to
clawed_back - Debits partner holdback entries oldest-first per
applyClawback - If the reserve is insufficient, logs the shortfall + flags for Fraud Ops review
Disputes (charge.dispute.created) flip a pending conversion to pending_review, so it can't be approved mid-dispute. When the dispute closes (charge.dispute.closed), a lost dispute claws the commission back like a refund, and a won one returns the conversion to pending for you to approve. A conversion the risk rules also flagged stays with Apex's review.