How to Handle Affiliate Commissions on Stripe Refunds, Upgrades, and Downgrades
A B2B SaaS paying $2,000/month in affiliate commissions on Stripe is exposed to roughly $300–500/month in unrecovered clawbacks unless the commission system explicitly listens to refunds, partial refunds, upgrades, and downgrades.
Muzahid Maruf, Founder

How to Handle Affiliate Commissions on Stripe Refunds, Upgrades, and Downgrades
A B2B SaaS paying $2,000/month in affiliate commissions on Stripe is exposed to roughly $300–500/month in unrecovered clawbacks unless the commission system explicitly listens to refunds, partial refunds, upgrades, and downgrades.
On this page
- 01The refund window vs payout cycle problem
- 02Stripe events that trigger commission changes
- 03Commission lifecycle states
- 04Upgrade commission math
- 05Downgrade commission math
- 06NET-30 refund window
- 07Partial refunds (proportional commission)
- 08Common mistakes
- 09Refund type to clawback math
- 10How TrackRev handles this
A B2B SaaS paying $2,000/month in affiliate commissions on Stripe is exposed to roughly $300–500/month in unrecovered refund clawbacks unless the commission system explicitly listens to refunds, partial refunds, upgrades, and downgrades. The default "pay 20% on every paid invoice" workflow ignores the events that change the underlying revenue, so commission keeps flowing to affiliates on customers whose Stripe charges were partially refunded, downgraded, or canceled — and quietly under-pays affiliates on customers who upgraded. This guide covers the specific Stripe events that should trigger commission changes, the lifecycle states a robust system runs through, worked examples on upgrade and downgrade math, and the common implementation mistakes that produce 8–12% commission leakage.
Key takeaway
Commission lifecycle is a Stripe webhook problem disguised as a finance problem. The events that matter — charge.refunded, charge.refund.updated, customer.subscription.updated, customer.subscription.deleted, invoice.payment_failed — each map to a specific commission action. Wire all five and your clawback exposure drops to single digits. Wire none and you'll leak 8–12% of commissions on revenue that didn't actually collect.
The refund window vs payout cycle problem
Most affiliate programs default to paying commissions monthly — usually Net-30 from invoice paid. That sounds reasonable until you map it against the refund distribution: 60–75% of SaaS refunds happen in the first 14 days after charge, with a long tail extending to 60–90 days for annual plans with prorated refund policies. A Net-30 payout cycle pays the commission before the customer's refund window has closed, which means refund-driven clawbacks must be collected retroactively.
Retroactive clawbacks have two problems. First, they're often uncollectable — once you've paid the affiliate, asking for the money back creates friction even when the policy clearly allows it. Second, they're a poor signal: the affiliate sees a credit-balance line on next month's payout, often without context, and concludes the program is unreliable. The right structure is to hold commissions through the refund window plus a buffer, then pay only what's actually clear.
Stripe events that trigger commission changes
Five Stripe events drive the commission lifecycle. Each one corresponds to a specific commission action that needs to be in your webhook handler.
| Event | What it means | Commission action |
|---|---|---|
invoice.paid | Customer paid an invoice (subscription or one-time) | Create commission record in Pending state |
charge.refunded | Full refund issued on a charge | Reverse commission entirely (Pending → Reversed, or clawback if Paid) |
charge.refund.updated | Partial refund (or refund amount adjusted) | Recalculate commission proportionally to remaining net revenue |
customer.subscription.updated | Plan change (upgrade or downgrade) | Adjust recurring commission base; handle proration |
customer.subscription.deleted | Subscription canceled | Stop future recurring commission accrual |
invoice.payment_failed | Invoice payment failed (dunning) | Move commission to Held state until retry succeeds or fails |
Source: Stripe API documentation, Q2 2026. Event names are the production Stripe webhook event types; <code>checkout.session.completed</code> is intentionally not on this list — see common-mistakes section below.
<code>charge.refunded</code> (full refund)
When Stripe fires charge.refunded, the entire charge has been refunded to the customer. The commission attached to that charge should be reversed entirely. If the commission was still in Pending state (within the hold period), the state transitions Pending → Reversed and the affiliate sees no payout for that conversion. If the commission was already Paid, the reversal becomes a clawback — typically applied as a credit balance against the affiliate's next payout.
<code>charge.refund.updated</code> (partial refund proration)
The trickier event. A partial refund on a charge means part of the charge was refunded — say, $100 refunded on a $300 invoice. The commission should be recalculated against the remaining net: if the original commission was $60 (20% of $300), the adjusted commission is $40 (20% of $200). This event also fires when the refund amount itself is adjusted, so handlers must be idempotent and recompute from the current state rather than apply a delta.
<code>customer.subscription.updated</code> (upgrade or downgrade)
Fired whenever the subscription changes plan, quantity, or billing cycle. The event payload includes the previous attributes via previous_attributes, which is what lets the handler diff old vs new. The commission action depends on whether the change is an upgrade (increase the recurring commission base) or downgrade (decrease it), with proration handled on the partial month.
<code>customer.subscription.deleted</code> (cancellation)
When the subscription is canceled, future recurring commission accrual stops — but already-earned commissions for prior periods are not reversed. The handler should mark the subscription as canceled in the commission ledger and stop generating commission records on future invoice cycles. Past invoices that were already commissioned remain.
<code>invoice.payment_failed</code> (commission contingent on collected revenue)
When a renewal invoice fails — typically because the card on file declined — the commission for that period should not be paid until the payment succeeds via dunning. The handler should move the commission to a Held state when invoice.payment_failed fires, then move it to Pending when the retry produces invoice.paid, or to Reversed if the subscription ends up in customer.subscription.deleted after dunning failure.
Commission lifecycle states
A robust commission record runs through five states: Pending → Eligible → Approved → Paid → Reversed. Each state has a clear definition and a clear transition trigger.
- Pending — created on
invoice.paid. Inside the hold window. May be reversed if a refund event fires. - Eligible — hold window has expired. No refund or chargeback has occurred. Ready for payout approval.
- Approved — finance has reviewed and approved the payout batch. Awaiting transfer.
- Paid — funds have been transferred to the affiliate.
- Reversed — refund or cancellation occurred. May produce a clawback on next payout if state was already Paid.
Upgrade commission math
Worked example. A customer on a $99/month plan (20% commission = $19.80/month recurring) upgrades to a $299/month plan mid-cycle on day 15 of a 30-day billing period.
Stripe handles the proration: it charges the customer for the remaining 15 days at the higher rate, credits the unused 15 days at the lower rate. The net invoice for the upgrade is roughly $100 (a prorated catch-up charge). The next full invoice cycle is $299.
The commission handling: on the customer.subscription.updated event, identify the upgrade by diffing previous_attributes.items[0].price.id against the new price ID. For the current cycle, create a one-time commission on the proration invoice at the standard rate (20% × $100 = $20). For subsequent invoices, the recurring commission base becomes $299 instead of $99, so the affiliate now earns $59.80/month instead of $19.80/month going forward.
// Webhook handler for customer.subscription.updatedasync function handleSubscriptionUpdated(event) { const subscription = event.data.object; const previous = event.data.previous_attributes; // Detect a plan change by comparing price IDs const oldPriceId = previous?.items?.data?.[0]?.price?.id; const newPriceId = subscription.items.data[0].price.id; if (!oldPriceId || oldPriceId === newPriceId) return; const oldPrice = await stripe.prices.retrieve(oldPriceId); const newPrice = await stripe.prices.retrieve(newPriceId); const commission = await findCommissionBySubscription(subscription.id); if (!commission) return; // not from an affiliate if (newPrice.unit_amount > oldPrice.unit_amount) { // Upgrade: bump the recurring base for future invoices await updateCommissionBase(commission.id, newPrice.unit_amount); // The proration invoice fires its own invoice.paid event; // that handler creates a one-time commission at the standard rate. } else { // Downgrade: lower the recurring base await updateCommissionBase(commission.id, newPrice.unit_amount); }}Downgrade commission math
A customer on $299/month ($59.80/month commission at 20%) downgrades to $99/month effective the next billing cycle. From the next invoice onward, the recurring commission base is $99, so the affiliate earns $19.80/month — a 67% reduction.
There are two policy questions to settle upfront. First: do downgrades trigger a partial refund on the current cycle? If yes, the partial refund flows through charge.refund.updated and the handler above runs. If no (the customer simply pays $99 starting next cycle), there's no refund event — just the lower base on the next invoice.paid.
Second: do you grandfather affiliates on the original commission rate? Some programs honor the original tier for the lifetime of the customer regardless of plan changes, on the principle that the partner earned the conversion at the higher tier. Others reset to the new tier on every change. Pick a policy, document it in the affiliate agreement, and implement it consistently. The grandfathering policy is one of the most common sources of disputed commission calculations — silence in the agreement guarantees the dispute.
NET-30 refund window
Hold commissions in Pending state for the longer of (a) your refund window plus 7 days, or (b) the standard hold period for affiliate payouts in your jurisdiction. For most SaaS programs this is 30–45 days. The 7-day buffer captures the late-arriving refunds that don't quite fit in the standard window — disputes filed via the card network late, customer-service refund decisions made on a Friday and processed the following Monday.
After the hold expires, transition Pending → Eligible. The eligibility transition is what unlocks payout in the next payout cycle. Commissions that go through a refund event before eligibility transition directly to Reversed without ever paying out, which is the cleanest case.
Partial refunds (proportional commission)
The math: if commission is X% × invoice_total, recalculated commission is X% × (invoice_total − refunded_amount). For a $300 invoice with $100 refunded and a 20% commission, the original $60 becomes $40 after the partial refund.
Implementation requires idempotency. The charge.refund.updated event can fire multiple times (refund amount adjusted, refund metadata updated), so the handler must recompute from the current refund total rather than applying deltas:
async function handleChargeRefunded(event) { const charge = event.data.object; const refundedAmount = charge.amount_refunded; // cents const netAmount = charge.amount - refundedAmount; const commission = await findCommissionByCharge(charge.id); if (!commission) return; // not from an affiliate // Recompute, do not apply deltas const newCommissionAmount = Math.round( (commission.rate_bps / 10_000) * netAmount ); if (newCommissionAmount === 0) { await reverseCommission(commission.id, "full refund"); } else if (newCommissionAmount !== commission.amount) { await adjustCommission(commission.id, newCommissionAmount, "partial refund"); }}Common mistakes
Four implementation mistakes appear in roughly 80% of affiliate programs that we've audited during TrackRev onboarding. Each one is fixable in a single sprint.
Paying on <code>checkout.session.completed</code> instead of <code>invoice.paid</code>
The checkout.session.completed event fires when the checkout flow ends — including when the customer enters card details but the actual payment hasn't yet succeeded. Subscriptions with trial periods, payment methods that require additional authentication (3D Secure), or payment methods that don't immediately collect (SEPA, bank transfer) will fire checkout.session.completed with no collected revenue. Commissions paid on this event get paid on customers who never actually pay.
Switch to invoice.paid. The event fires only when Stripe has actually collected the money. Programs that make this change typically see their unrecovered commission rate drop 8–12% in the first quarter.
Ignoring <code>charge.refund.updated</code>
Programs handle full refunds via charge.refunded but miss the partial-refund path. charge.refund.updated fires for partial refunds and for any subsequent adjustment to refund amounts. Without a handler, partial refunds silently leave the commission at the original (higher) amount.
Not stopping recurring commission on cancellation
customer.subscription.deleted fires when a subscription is canceled, but if the commission ledger doesn't update its "recurring commission active?" flag, the next invoice cycle (which won't actually happen, since the subscription is canceled) doesn't fire, but the commission record sits in an indeterminate state. The cleaner pattern: on cancellation, explicitly mark the recurring commission stream as ended. No future invoice means no future commission, but the audit trail is unambiguous.
Treating an upgrade as a new conversion
If your affiliate tracking treats the proration invoice from an upgrade as a brand-new conversion, you'll pay flat commission on the proration on top of the recurring commission you should already be earning. Worse, you may attribute the "conversion" to whatever cookie is active at the time of the upgrade — which is almost never the original affiliate. The fix: identify proration invoices via the billing_reason field on the invoice (subscription_update or subscription_cycle) and route them to the upgrade handler, not the new-conversion handler.
The most expensive default
If you trigger commission payment on checkout.session.completed, you'll pay commissions on every signed-up-but-never-collected customer — including failed 3D Secure flows, abandoned trial-to-paid conversions, and bank-transfer holds that never settle. Switch to invoice.paid and your unrecoverable commission rate drops 8–12% on day one. This is the single highest-ROI change in the commission lifecycle.
Refund type to clawback math
A quick reference for the four most common refund scenarios. All assume a 20% commission rate; substitute your own.
| Refund type | Clawback formula |
|---|---|
| Full refund (in hold window) | Commission reversed before payout: net clawback = $0 (no funds moved) |
| Full refund (after payout) | Commission already paid: clawback = full original commission ($X × 20%) |
| Partial refund (in hold window) | Commission recomputed: new = 20% × (charge − refund); old reversed before payout |
| Partial refund (after payout) | Clawback = 20% × refund amount, applied as credit balance to next payout |
Source: Standard commission lifecycle pattern. The cleanest implementations hold commission for refund-window + 7 days, which moves nearly all clawbacks into the in-hold case.
The hold-window math
Hold paid commissions in a Pending state for the duration of your refund window plus 7 days. Most refunds happen in the first 14 days; this captures 95%+ of clawbacks without making affiliates wait artificially long. Programs running 14-day refund policies typically hold commissions for 21 days; programs running 30-day refund policies hold for 37. Communicate the hold window clearly in onboarding so partners aren't surprised by the gap between conversion and first payout.
How TrackRev handles this
TrackRev's Stripe affiliate tracking integration subscribes to all five events above out of the box. Commissions flow through Pending → Eligible → Approved → Paid with refund events automatically reversing or proportionally adjusting depending on partial-vs-full. Upgrade and downgrade math runs on the customer.subscription.updated handler with the proration logic above baked in. Affiliate payouts batches Eligible commissions into a single payout cycle (Net-30 default; configurable to match your refund window) with clawbacks applied as credit balances against future cycles.
For programs migrating from another tool: TrackRev imports historical commission state including holds, eligibility flags, and clawback balances, so you don't lose audit trail in the move. See the how it works page for the integration steps, or start on the free tier at pricing to test the event flow before committing.
For the broader context this fits into: attributing Stripe revenue to marketing channels covers the attribution layer underneath commission calculation. B2B affiliate marketing for high-ACV programs covers the commission shape these mechanics support. Affiliate program compliance for FTC and GDPR covers the contract layer that should document your commission policy. And scaling your affiliate program from 0 to $50K MRR covers when in the program lifecycle the commission-lifecycle hardening should land.
External references: Stripe webhook events documentation; Stripe subscription upgrade and downgrade documentation; Stripe refunds documentation. The event names and proration behavior above are stable as of the 2024 Stripe API version and are the current reference for all the major affiliate platforms.
Frequently asked questions
- What about Stripe Tax effects on commission calculations?
- Commission should be calculated on the pre-tax revenue, not the total invoice including tax. Stripe Tax breaks the invoice into <code>subtotal</code> (pre-tax) and <code>total</code> (including tax) — your commission handler should use <code>subtotal</code>. Calculating against the total over-pays the affiliate by the tax rate, which is typically 5–25% depending on jurisdiction.
- How do I handle annual vs monthly upgrades?
- A monthly → annual upgrade typically charges a year upfront. The commission base for that invoice is the full annual amount; the recurring base going forward is the annual cycle. Run the commission as: flat commission on the annual invoice (if your structure includes flat), plus 20% × annual amount paid once at the annual renewal. Do not pay monthly recurring commission on top — that double-pays. The annual → monthly downgrade is the reverse: the next annual invoice doesn't happen; instead, monthly commissions resume at the monthly base.
- What if a partial refund makes the commission negative?
- This can happen if the customer was refunded in excess of the commissionable revenue — for example, due to manual goodwill credits. Floor the commission at zero rather than letting it go negative. If the program also has an outstanding positive commission balance for the same affiliate, apply the clawback against that balance; if not, hold the clawback as a credit balance against future payouts.
- How do I communicate clawbacks to affiliates?
- Every payout statement should itemize the gross commission, the clawbacks (with reasons), and the net. The most common complaint about programs that handle clawbacks well is opacity — affiliates can accept the clawback policy but resent surprise deductions. A payout statement that says "Gross commission: $1,200; Clawback (refund on customer Acme Corp, $400 invoice): −$80; Net: $1,120" is what makes the policy feel fair.
- How does TrackRev handle this automatically?
- TrackRev's <a href="/features/stripe-affiliate-tracking">Stripe affiliate tracking</a> subscribes to all five Stripe webhook events (<code>invoice.paid</code>, <code>charge.refunded</code>, <code>charge.refund.updated</code>, <code>customer.subscription.updated</code>, <code>customer.subscription.deleted</code>, plus <code>invoice.payment_failed</code> for dunning) and runs the lifecycle pattern above out of the box. Commission state, hold windows, upgrade/downgrade math, partial-refund proration, and clawback tracking are all native. Payouts go through <a href="/features/affiliate-payouts">affiliate payouts</a> with itemized statements that include clawbacks and reasons — so the operational discipline of the patterns above ships as a default rather than an integration project.
- What happens if Stripe retries a webhook delivery — will I create duplicate commissions?
- Stripe retries webhooks on non-2xx responses for up to 3 days. Your handler must be idempotent: use the Stripe event ID as a deduplication key, so a retried delivery doesn't create a second commission record. The standard pattern is to record the event ID on commission creation and reject any subsequent attempt to create a commission for the same event ID. TrackRev's handlers implement this natively.
- Should I notify the affiliate when a commission is reversed?
- Yes — silent reversals are the single most common source of partner trust failures. Send an email (or in-app notification) when a commission moves from Pending or Paid to Reversed, with the reason (full refund, partial refund, cancellation), the original commission amount, and the new amount. The notification turns a confusing payout statement line item into a transparent operational event.

Written by
Muzahid Maruf, Founder, TrackRev.io & Contant.io
Muzahid Maruf is the founder of TrackRev.io and Contant.io. He writes about marketing attribution, link tracking, and revenue analytics for SaaS teams.
Keep reading
Related articles from the TrackRev blog.

How to Attribute Stripe Revenue to Marketing Channels (2026 Guide)
10 minHow to Attribute Stripe Revenue to Marketing Channels (2026 Guide)

B2B SaaS Attribution: How Long Sales Cycles Break Standard Tracking and How to Fix It
10 minB2B SaaS Attribution: How Long Sales Cycles Break Standard Tracking and How to Fix It

Attribution Fraud: How Affiliates Game Click Counts and How to Stop It
10 minAttribution Fraud: How Affiliates Game Click Counts and How to Stop It