DEV Community

Mihir kanzariya
Mihir kanzariya

Posted on

Recurring affiliate commissions on Stripe: the edge cases nobody warns you about

Every tutorial on building affiliate tracking with Stripe covers the happy path: user clicks a referral link, signs up, pays, affiliate gets a commission. Done.

But if you are building recurring commissions for a SaaS product, the happy path is maybe 60% of what actually happens. The other 40% is edge cases that will either cost you money, break affiliate trust, or both.

Here is what I ran into building commission logic on top of Stripe's billing API, and how I handled each case.

1. Plan changes mid-cycle

A customer upgrades from $20/mo to $50/mo halfway through their billing cycle. The affiliate was earning 30% on the $20 plan. Do they now earn 30% on $50? On the prorated amount for this cycle? On the full $50 starting next cycle?

The answer depends on how you listen to Stripe events. If you use invoice.paid (which you should for recurring), you will get the prorated amount for this cycle. The affiliate's commission naturally adjusts because it is always a percentage of what was actually charged.

The mistake I see: using customer.subscription.updated to recalculate commissions. That event fires before payment, so you end up crediting commissions on amounts that might fail to charge.

// Good: commission based on what was actually paid
stripe.webhookEndpoints.create({
  enabled_events: ['invoice.paid'],
});

// Inside your webhook handler
const amountPaid = invoice.amount_paid; // actual charged amount
const commission = Math.round(amountPaid * commissionRate);
Enter fullscreen mode Exit fullscreen mode

2. Failed payments and dunning

Stripe retries failed payments over a configurable window (Smart Retries or your own schedule). During that window, the subscription is past_due but not cancelled.

Should the affiliate earn commission on a payment that fails three times and then succeeds on the fourth retry? Yes, because the customer paid. Should they earn commission if the payment never succeeds and the subscription cancels? Obviously not.

The key insight: if you only listen to invoice.paid, this handles itself. You never credit a commission until money actually moves. The problem comes when you try to get clever with invoice.payment_failed and preemptively clawback. Do not do that. Wait for the final outcome.

3. Refunds inside vs outside the attribution window

You offer a 14-day refund policy. A customer gets a refund on day 10. The affiliate already earned commission on that payment. You need to clawback.

But what about a refund on day 45? Or day 90? At some point, the refund is a customer service decision that should not penalize the affiliate who brought a real, paying customer.

I use a simple rule: clawback commissions on refunds within the first 30 days of the specific payment. After that, the commission stands. This is fair to affiliates and prevents the situation where a 6-month customer gets a partial refund and the affiliate loses their commission retroactively.

// On charge.refunded event
const charge = event.data.object;
const paymentDate = new Date(charge.created * 1000);
const refundDate = new Date();
const daysSincePayment = (refundDate - paymentDate) / (1000 * 60 * 60 * 24);

if (daysSincePayment <= 30) {
  // Clawback the commission
  await clawbackCommission(charge.id);
} else {
  // Commission stands, log for transparency
  await logRefundNoClawback(charge.id, daysSincePayment);
}
Enter fullscreen mode Exit fullscreen mode

4. Annual vs monthly: same rate or different?

A customer pays $200/year instead of $20/month. The annual plan is a discount ($240 annualized vs $200 paid). If the affiliate rate is 30%, do they earn $60 upfront or $6/month?

Most programs pay the commission on what was actually charged. So the affiliate gets 30% of $200 = $60 as a single commission when the annual invoice pays. This is simpler to implement and aligns incentives: the affiliate helped close a higher-commitment customer.

The trap: if you pay $60 upfront on annual and the customer refunds at month 3, you need to clawback $60, not $15. Annual commissions carry more clawback risk, so your refund window matters more.

5. The free trial attribution gap

Customer clicks an affiliate link, starts a 14-day free trial, and converts to paid on day 15. If your attribution only fires on checkout.session.completed, you captured the affiliate at trial start. Good.

But what if the trial does not require a payment method? Then checkout.session.completed might not fire at all during the trial. The customer adds their card later through the billing portal. If you are not careful, that second step has no affiliate context attached.

The fix: store the affiliate attribution in your own database at signup time (not in Stripe metadata alone). When the subscription transitions from trialing to active, look up the affiliate from your records, not from the Stripe event.

6. Multi-currency edge cases

Your product charges in USD, EUR, and GBP. An affiliate in the US refers a customer who pays in EUR. The commission is calculated as 30% of the EUR amount. But when you pay the affiliate, you pay in USD.

Do you convert at the exchange rate when the customer paid, or when you run payouts? If you batch payouts monthly, the exchange rate can shift 2-3% in either direction.

Pick one: convert at payment time and store the USD-equivalent commission immediately, or convert at payout time and accept the variance. I convert at payment time because it makes the affiliate's dashboard accurate in real-time. They see exactly what they earned without waiting for payout-day exchange rates.


None of these problems are hard individually. But if you do not think about them upfront, you end up with affiliate disputes, incorrect payouts, and patching logic months after launch.

The general principle: always base commissions on invoice.paid (real money moved), store attribution in your own database (not just Stripe metadata), and define your clawback rules before your first affiliate asks about them.

I build affiliate tracking for SaaS products on Stripe. If you are working on something similar, I wrote about cookie-less affiliate attribution on Stripe previously.

Top comments (0)