How to handle Stripe affiliate commissions on refunds, upgrades and downgrades
How a Stripe affiliate commission should react to partial refunds, upgrades, downgrades and cancellations, with the invoice arithmetic worked out.
Muzahid Maruf, FounderUpdated
On this page
- 01How should a Stripe affiliate commission react to each event?
- 02How does refund math work with tax on the invoice?
- 03How do upgrades and downgrades change the invoice?
- 04What happens on annual switches, cancellations and failed renewals?
- 05How do you test these cases with Stripe test clocks?
- 06How do you keep channel revenue honest after refunds and upgrades?
- 07How do other affiliate tools handle these events?
Explore with AI
Opens this article inside the chosen assistant with a ready-made prompt.
A Stripe affiliate commission is a percentage of the cash a customer paid and kept.
Stripe Billing records the payment on invoice.paid, so create the commission there, base it on the pre-tax amount, and shrink it in proportion on charge.refunded, which covers partial refunds too.
With cash as the base, upgrades, downgrades and coupons adjust it through Stripe's own invoice math, and cancellation only stops future commissions.
The worked examples below cover a refund that cuts a $60.00 commission to $40.00 and an upgrade from $99 to $299 that pays $125.87 across 90 days.
The numbers rest on Stripe's 2026 documentation for 8 webhook events, and the last section shows where TrackRev differs.
Key takeaways
- Create the commission on invoice.paid from the pre-tax cash paid, so upgrades, coupons and downgrade credits flow through Stripe's own invoice amounts.
- One event, charge.refunded, covers full and partial refunds. Scale the commission by the share of the charge the customer kept: a $110.00 refund on a $330.00 charge turns a $60.00 commission into $40.00.
- A downgrade sends no refund event. Stripe credits the next invoice, so a plan-price commission overpays by 20% of the credit, which is $13.33 on a $66.67 credit.
How should a Stripe affiliate commission react to each event?
| Event | What Stripe says it means | Commission action |
|---|---|---|
invoice.paid | Payment succeeded | Create the commission |
charge.refunded | Refunded, partially or fully | Recompute from the running amount_refunded |
refund.failed | A refund failed | Restore the reversed commission |
customer.subscription.updated | Plan, coupon or renewal change | Only for per-plan rates or caps |
customer.subscription.deleted | Subscription ended | Stop future commissions |
invoice.payment_failed | Payment attempt failed | Nothing: no paid invoice, no commission |
charge.dispute.created, charge.dispute.closed | Dispute opened or closed | Reverse on open, restore on a win |
checkout.session.completed | Checkout finished | Do not pay on it |
The webhook types that touch a commission, with meanings paraphrased from Stripe's event type reference.
Eight Stripe event types touch a commission, and the table sets the action for each. Of the 8 rows, 4 change the amount (invoice.paid, charge.refunded, refund.failed and the dispute pair) and 2 trigger nothing (invoice.payment_failed and checkout.session.completed).
The trigger is invoice.paid, which means Stripe collected the money, and checkout completion is not that signal. checkout.session.completed can fire before money moves in Stripe Checkout. Stripe's fulfillment guide gives ACH Direct Debit as an example, with checkout.session.async_payment_succeeded following later.
In Stripe Checkout, pay on invoice.paid for subscriptions and payment_intent.succeeded for one-time payments.
The refund trigger is charge.refunded, the event Stripe describes as covering partial refunds. The charge.refund.updated event fires only on selected payment methods, so a handler built on it alone can miss them.
Stripe's webhook documentation adds that an endpoint can receive the same event twice and that live deliveries retry for up to 3 days, so recompute from current totals and key each write on an ID.
The guide to affiliate refund clawbacks covers replays, disputes and holds.
How does refund math work with tax on the invoice?
Refund math with tax means scaling the commission by the share of the charge the customer kept.
Tax belongs to the tax authority, so a commission on the gross charge overpays the affiliate by the tax rate, while a refund is gross because it returns the tax too.
Take a $300.00 invoice plus $30.00 of sales tax from Stripe Tax, a 20% commission of $60.00 on the pre-tax amount, and two refunds against the $330.00 charge.
| Refund | Refunded so far | Left after reversing 20% of each refund | Left after scaling by what the customer kept |
|---|---|---|---|
| $110.00 | $110.00 | $38.00 | $40.00 |
| $220.00 | $330.00 | -$6.00 | $0.00 |
The scaled figure is $60.00 x (1 - refunded / $330.00).
Scaling needs no tax lookup, because the commission was already pre-tax. Reversing 20% of each refund takes back $22.00 and $44.00, which is $6.00 more than the $60.00 credited.
The scaled method assumes a refund returns the $300.00 pre-tax amount and the $30.00 tax in the original ratio. Floor the result at $0.00 either way.
async function onChargeRefunded(event) { const charge = event.data.object; // amount_refunded is the running total const commission = await findCommissionByCharge(charge.id); if (!commission) return; // not an affiliate sale const kept = 1 - charge.amount_refunded / charge.amount; const next = Math.max(0, Math.round(commission.original_cents * kept)); // event.id is the idempotency key: a repeat delivery changes nothing if (next !== commission.current_cents) { await setCommission(commission.id, next, event.id); }}How do upgrades and downgrades change the invoice?
A proration is a credit or debit line that Stripe adds to an invoice when a plan changes.
By default, create_prorations adds a credit line for unused time on the old price and a debit line for the remaining time on the new one, and bills neither at once.
Setting always_invoice bills an upgrade immediately, per the subscription update reference. A downgrade sends no refund: the proration guide says negative prorations are not automatically refunded, and the customer balance guide lists them as credit against the next invoice.
With no charge.refunded event, the affiliate keeps the larger $299 plan's commission.
Take one customer: a $99 plan upgraded to $299 on day 15 of a 30-day cycle, then dropped back to $99 on day 50.
| Change | Credit for old plan | Debit for new plan | Net |
|---|---|---|---|
| Day 15, $99 to $299, 15 days left | -$49.50 | $149.50 | $100.00 |
| Day 50, $299 to $99, 10 days left | -$99.67 | $33.00 | -$66.67 |
Stripe prorates to the second, so live invoices differ by a few cents from these 2 rows.
| Day | Stripe event | Invoice | Cash commission at 20% | Plan-price commission at 20% |
|---|---|---|---|---|
| 0 | Signup on $99 | $99.00 | $19.80 | $19.80 |
| 15 | Upgrade, prorations queued | None | $0.00 | $0.00 |
| 30 | Renewal with the $100.00 proration | $399.00 | $79.80 | $59.80 |
| 50 | Downgrade, credit queued | None | $0.00 | $0.00 |
| 60 | Renewal after the $66.67 credit | $32.33 | $6.47 | $19.80 |
| 90 | Renewal | $99.00 | $19.80 | $19.80 |
| Total | $629.33 | $125.87 | $119.20 |
Default proration settings, tax ignored.
Should a commission follow cash or the plan price?
A commission should follow cash, not the plan price. With always_invoice on the upgrade, the $100.00 is invoiced on day 15 and the day 30 invoice is $299.00, so the commission total stays $125.87 and only the timing moves.
The plan-price rule is $20.00 short on the upgrade and pays $13.33 on credited money at the downgrade, so across 90 days the cash rule pays $125.87 and the plan-price rule $119.20, a $6.67 gap from two errors pulling in opposite directions.
in our experience building TrackRev I would pay on cash, because Stripe has already done the proration arithmetic and a plan-price rule makes you redo it. In your own code the cash figure is amount_paid less the invoice's tax.
To exclude proration, read parent.subscription_item_details.proration on each invoice line.
When does the subscription event matter?
The customer.subscription.updated event is the Stripe webhook that matters when a rule depends on the plan. A percentage of cash needs no plan lookup, but per-plan rates, a tier bonus or a cap such as the first 12 months do.
For example, a 30% rate on the $299 plan and 20% on the $99 plan needs the price ID from this event.
Stripe's subscription webhook guide says customer.subscription.updated also fires for renewals, coupons and added invoice items, so diff previous_attributes against the new object, which per the event object reference holds the whole array when part of one changes.
function planChange(event) { const before = event.data.previous_attributes?.items?.data?.[0]?.price?.id; const after = event.data.object.items.data[0].price.id; // renewals and coupons arrive here too, with no price change return before && before !== after ? { before, after } : null;}What happens on annual switches, cancellations and failed renewals?
An annual switch in Stripe Billing bills the new price at once. Stripe's price change guide says that when two prices have different billing intervals, the new price bills from the day of the change.
Take a customer on $99 a month who moves to $990 a year on day 10 of 30.
| Line | Amount |
|---|---|
| Credit for 20 unused days of the $99 plan | -$66.00 |
| Annual plan | $990.00 |
| Invoice | $924.00 |
| Commission at 20% | $184.80 |
No invoice follows for 12 months, so a rule that pays 12 months of recurring commission is met by that one payment. Count the window in months of service, because counting invoices pays the annual customer 12 times over.
The customer.subscription.deleted event reverses nothing, because only money returned to the customer undoes a paid invoice. A declined renewal needs no hold in Stripe Billing when commissions are created on invoice.paid, and Stripe marks the subscription past_due while it retries.
How do you test these cases with Stripe test clocks?
A Stripe test clock is a sandbox clock that moves a customer through billing cycles in minutes, so the 6 tests below can cover 90 days of invoices on a $99 plan.
Stripe's test clock simulations advance time in a Stripe sandbox by up to 2 billing intervals per step, so a monthly subscription jumps up to 2 months, and the Run simulation button is disabled for a customer attached to more than 3 subscriptions.
| Test | Expected result |
|---|---|
| Subscribe at $99 a month through a test affiliate link | One commission of $19.80 |
Upgrade to $299 on day 15, once with always_invoice and once with the default | Both runs total $79.80 by day 30 |
| Downgrade on day 50 and advance to day 60 | Commission of $6.47 and no refund event |
| Refund $110.00, then $220.00, on a $330.00 charge | Ledger lands on $0.00 |
| Refund a charge from an earlier cycle | Commission reverses even though newer charges exist |
| Resend a refund event from the Stripe Dashboard | Ledger does not change |
How do you keep channel revenue honest after refunds and upgrades?
Honest channel revenue means debiting every refund from the channel that earned the sale.
Debit a refund from the channel that earned the sale, using the model that credited it, whether last-touch, first-touch or linear, so a linear sale splits its refund across the same touches.
Credit an upgrade from the source stored against the customer, because a plan change in the Stripe Customer Portal happens on a Stripe-hosted page you cannot tag, as Stripe Customer Portal attribution explains.
Normalize annual plans to monthly figures first. Moving from $99 a month to $990 a year books $891 of expansion if counted raw, while the monthly equivalent falls from $99 to $82.50.
TrackRev copies the original click's credit onto each renewal, rescaled to the charge amount, so expansion revenue stays with the channel that brought the customer.
How do other affiliate tools handle these events?
Affiliate tools handle refunds in 2 ways: proportional recalculation, as Rewardful does, or a negative commission keyed on an event ID, as FirstPromoter does.
Rewardful's Stripe integration article says it commissions the amount the customer actually paid, after discounts and account balance debits, and recalculates from the refunded amount on full and partial refunds.
FirstPromoter's refund endpoint creates a negative commission, requires an event ID, and answers a repeat with a 409. In our comparison, Rewardful documents proportional recalculation on partial refunds, while TrackRev reverses the whole commission on a refund of any size.
FirstPromoter needs one event ID per refund, so a replay gets an HTTP 409 instead of a second debit. The Rewardful and TrackRev comparison goes further.
Sources: help.rewardful.com/en · docs.firstpromoter.com/api-reference-v2
What does TrackRev do with each case?
TrackRev is SaaS affiliate software for indie hackers and small SaaS teams, and we built TrackRev's Stripe sync to read charges, not subscription events, using a restricted API key.
Our own sync runs hourly, and an optional webhook for charge.succeeded, charge.refunded and checkout.session.completed runs it at once. The Stripe integration page lists the key permissions.
| Case | What TrackRev does |
|---|---|
| Paid charge | Each paid charge from a partner link or coupon earns its own commission, renewals included on a recurring program |
| Commission base | The charge amount Stripe reports, tax included: 20% of a $330.00 charge is $66.00 |
| Upgrade or downgrade | The commission follows the charge: the $399.00 and $32.33 renewals above earn $79.80 and $6.47 at 20%, with no per-plan rate |
| First-sale-only program | A $299 renewal earns $0.00 after the first sale |
| Refund of any size | A $110.00 refund on that $330.00 charge reverses all $66.00, at every multi-tier level |
| Commission already paid | Marked refunded with no negative balance, so subtract it from the next payout |
| Dispute | Follows the refund status Stripe reports, so check the ledger when a dispute ends without a refund |
| Hold period | Per program, 0 to 180 days, default 0 |
TrackRev affiliate program behavior with a Stripe connection.
TrackRev reverses the whole commission on any refund and has no per-plan rates, so pick a tool that documents proportional reversal, as Rewardful does, if you need it.
The affiliate program needs a paid plan from Indie at $29 a month, with no cap on the commission it tracks and 0% fees. The Free plan covers link tracking only, with 50 links and 1,000 tracked events a month.
The pricing page, the affiliate program page and affiliate tracking for recurring subscriptions cover the rest.
Found this useful? Share it.
Frequently asked questions
- No. Tax goes to the tax authority, so pay on the pre-tax amount. Stripe's invoice has total_excluding_tax and total fields, so on a $120.00 total with $20.00 of tax, a 20% commission is $20.00, not $24.00.
- Yes, when the commission is a share of cash collected. A $99 invoice with a 25% coupon is $74.25, so a 20% commission is $14.85.
- charge.refunded carries the charge and its running amount_refunded, while refund.created carries one refund and its ID. Recompute from the charge and deduplicate on the refund ID, since a $330.00 charge can take several refunds.
- Yes, if your partner terms say so. A $99 first invoice earns $19.80 at 20% and still reverses if refunded, while a $299 renewal after an upgrade earns $0.00.
- At least as long as your refund window, so a 30-day policy needs a hold of 30 days or more. TrackRev's hold runs 0 to 180 days per program and starts at 0.
- No. The Free plan covers link tracking only, with 50 links and 1,000 tracked events monthly. Commission tracking starts on the $29 a month Indie plan.
- It shrinks the commission by the refunded share. On a $50.00 pre-tax charge, a $10.00 commission becomes $5.00 after a $25.00 refund and $0.00 after a $50.00 refund, because the customer kept 50% and then 0% of the money.

Written by
Founder, TrackRev.io & Contant.io
Muzahid Maruf founded TrackRev.io, SaaS affiliate software with no limit on tracked revenue, and Contant.io. He writes about affiliate programs.
Writes about Marketing attribution · Link tracking · Revenue analytics · SaaS growth
Run your affiliate program without a revenue cap.
Affiliate software for indie hackers and small SaaS teams: $29 a month flat, 0% fees, link tracking and revenue attribution built in. Free to start, no card.