Most affiliate-tracking tutorials stop at "calculate the commission." Then you are sitting on a table of amounts you owe 40 people and no clean way to actually send the money. Manual PayPal and bank transfers work for three affiliates and fall apart at thirty, and now you also own their tax forms and KYC.
Stripe Connect handles that side. Your affiliates onboard once, Stripe verifies them and collects tax info, and you move money to them with an API call. Here is the shape of it.
One connected account per affiliate
Create an Express account when an affiliate joins. Express means Stripe hosts the onboarding and dashboard, so you never touch their bank details.
const account = await stripe.accounts.create({
type: 'express',
email: affiliate.email,
capabilities: { transfers: { requested: true } },
metadata: { affiliate_id: affiliate.id },
});
// store account.id against your affiliate record
Then send them through onboarding with a one-time link:
const link = await stripe.accountLinks.create({
account: account.id,
refresh_url: 'https://yourapp.com/affiliate/reauth',
return_url: 'https://yourapp.com/affiliate/done',
type: 'account_onboarding',
});
// redirect the affiliate to link.url
Only pay accounts that are actually ready
Do not assume onboarding finished because they clicked through. Check the account before every payout. An affiliate who abandoned KYC will fail the transfer and leave you reconciling a mess.
const acct = await stripe.accounts.retrieve(affiliate.stripe_account_id);
if (!acct.payouts_enabled) {
// nudge them to finish onboarding, skip this run
return;
}
Move the money
Once commission clears its hold and the account is ready, create a transfer from your platform balance to the affiliate:
await stripe.transfers.create({
amount: commissionCents, // what you owe, in cents
currency: 'usd',
destination: affiliate.stripe_account_id,
transfer_group: `payout_${payoutRun.id}`,
metadata: { affiliate_id: affiliate.id, run: payoutRun.id },
}, { idempotencyKey: `transfer_${affiliate.id}_${payoutRun.id}` });
The idempotencyKey matters. Payout jobs get retried, and without it a timeout that actually succeeded server-side turns into a double payment. Key it to the affiliate plus the run, not the wall clock.
The gotchas that bite in production
- Fund the platform balance. Transfers pull from your Stripe balance. If it is empty because payouts to your own bank already swept it, the transfer fails. Keep a buffer or use a separate payout schedule.
- Hold past the refund window. Do not transfer commission on an invoice that can still be refunded. Pay on a delay (for example, monthly for the prior month) so a refund reverses a ledger row instead of clawing back real money already sent.
-
Batch, but track per affiliate. One
transfer_groupper run, one idempotency key per affiliate, so a partial failure is easy to retry without touching the affiliates who already got paid. - Currency. Transfers settle in the destination account's currency. If you charged in EUR and pay in USD, decide where the conversion happens and be consistent, or your ledger will not reconcile.
The calculation side is where the logic lives, but the payout side is where the money actually leaves, and it is the part that generates angry emails when it is wrong. Connect turns "I owe 40 people money" into one idempotent job.
We built this into Referralful (affiliate software on Stripe, which I help build) so the onboarding, holds, and transfers run without anyone touching a spreadsheet. Even rolling your own, Express accounts plus idempotent transfers are the two pieces to get right first.
Top comments (0)