Free audit · Bootstrapped Founder
Application fee leaks: keeping a cut of a refunded sale
Your platform takes 10% of every transaction. A customer gets a full refund. Do you still have their 10%? By default, yes — and depending on your terms, that is either a deliberate policy or a compliance exposure you did not know you had.
The default nobody reads
refund_application_fee defaults to false. Refund a charge without setting it and your platform keeps its full fee on revenue that no longer exists.
This cuts both ways, and which way matters depends on your terms of service.
Some platforms keep the fee deliberately — a stated policy agreed with sellers, compensating for payment processing already incurred. That is a legitimate business decision.
Others do it unknowingly, which is a trust and compliance problem: charging a percentage on transactions that were reversed, usually contradicting their own published terms.
And some over-correct, refunding the fee explicitly *and* using reverse_transfer — which already returns the funds that financed the fee. That combination can give the same money back twice.
The proportional calculation
The share of the fee to return equals the share of the charge refunded:
shouldRefund = round((amount_refunded / amount) × application_fee.amount)
Stripe rounds half-up on its own proportional calculations. Matching that rounding is what keeps your reconciliation agreeing with Stripe to the cent rather than drifting by one on every partial refund.
- Charge amount
- $250.00
- Application fee collected
- $25.00
- Refunded to buyer
- $100.00 (40%)
- Fee that should be returned
- $10.00
- Fee actually returned by default
- $0.00
- Unreturned fee
- $10.00
Why checking the fee alone gives a wrong answer
Both reverse_transfer and refund_application_fee touch the same dollars, so examining either in isolation produces a confidently incorrect result.
The measure that holds is the platform's net position across both:
Net Platform Margin = (Collected Fee − Refunded Fee) − (Original Transfer − Reversed Transfer)
A negative figure means you are genuinely out of pocket. A positive one means you are whole, regardless of which lever got you there. A tool that flags an unrefunded fee on a charge already made whole by a transfer reversal is double-counting — and if it acts on that automatically, it double-refunds the seller.
const netMargin =
(applicationFee.amount - applicationFee.amount_refunded) -
(transfer.amount - transfer.amount_reversed);
// netMargin < 0 → you are carrying the lossThe zero-decimal currency trap
JPY, KRW, VND and a dozen others have no minor unit. An amount of 1000 means ¥1000, not ¥10.00.
Dividing by 100 for display is wrong by a factor of 100 for these currencies. A reconciliation script that gets this wrong reports losses two orders of magnitude off — which is either an alarming false positive or a silently ignored real one.
What FeeGuard does about it
FeeGuard computes the net position before attributing a shortfall to any cause. When reverse_transfer was used and the platform is already whole, the fee gap is not reported at all.
Zero-decimal and three-decimal currencies are handled explicitly, so a ¥ finding reads correctly rather than being off by 100×.
Run the check yourself
Seven steps, no signup. If the number at step 6 is greater than zero, you have a leak — and you did not need us to prove it.
- 1
Export all charge.refunded events for the last 90 days.
- 2
For each charge that has a transfer, retrieve the transfer and sum reversals.
- 3
Calculate expected reversal = (amount_refunded / amount) × transfer.amount. Flag if actual < expected.
- 4
For every application_fee on those charges, confirm amount_refunded is proportional.
- 5
Pull all charge.dispute.closed with status=lost; verify transfer reversal + fee refund occurred.
- 6
Sum missing amounts. That number is your recoverable baseline.
- 7
Connect FeeGuard if you would rather this ran continuously than once.