refund_application_fee — returning your platform cut
refund_application_fee returns your platform cut when a charge is refunded. Skip it and you keep a fee on revenue that no longer exists. Here is the math.
What the flag does
refund_application_fee: true refunds the proportional share of the application fee your platform collected on the charge.
Like reverse_transfer, it defaults to false. The platform keeps its fee unless it explicitly gives it back.
Why it cuts both ways
Some platforms deliberately keep the fee on refunds — a stated policy, agreed with their sellers. That is a legitimate business decision.
Others do it unknowingly, which is a trust and compliance exposure: they are charging a percentage on transactions that were reversed.
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 precedence rule
Because both levers touch the same dollars, checking either in isolation gives a wrong answer. The correct measure is the platform's net position across both:
Net Platform Margin = (Collected Application Fee − Refunded Application Fee) − (Original Transfer Amount − Reversed Transfer Amount)
A negative figure means the platform is genuinely out of pocket on the charge. A positive one means it is whole, regardless of which lever got it there.
const netMargin =
(applicationFee.amount - applicationFee.amount_refunded) -
(transfer.amount - transfer.amount_reversed);
// netMargin < 0 → the platform is carrying the lossHow FeeGuard handles it
FeeGuard computes the net position before attributing a shortfall to a cause. When reverse_transfer was used and the platform is already whole, the fee gap is not reported — flagging it would double-count, and an automated clawback acting on it would genuinely double-refund the connected account.
The proportional calculation, exactly
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 and slowly accumulating a discrepancy nobody can explain.
On a $250 charge with a $25 fee, a $100 partial refund (40%) means $10.00 of fee should come back. Left unset, you keep all $25 on a sale worth $150.
Zero-decimal currencies will break your script
JPY, KRW, VND, CLP and around 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 misses this reports either an alarming false positive or, worse, silently under-reports a real finding by two orders of magnitude.
Three-decimal currencies exist too — BHD, JOD, KWD, OMR, TND are quoted in thousandths. Any code that assumes two decimal places is wrong for roughly twenty currencies.
const ZERO_DECIMAL = new Set([
'bif','clp','djf','gnf','jpy','kmf','krw','mga',
'pyg','rwf','ugx','vnd','vuv','xaf','xof','xpf',
]);
function toMajorUnits(amount: number, currency: string): number {
return ZERO_DECIMAL.has(currency.toLowerCase()) ? amount : amount / 100;
}Deciding what your policy actually is
Before building detection for this, settle the policy question, because the same finding means opposite things depending on the answer.
If your terms say you keep the fee on refunds: unrefunded fees are correct, and the thing worth monitoring is the opposite case — fees returned when they should not have been, which is margin leaking the other way.
If your terms say you return it: every unrefunded fee is both lost trust and a contractual gap. Worth fixing retroactively, not just going forward.
If your terms do not say: that is the actual finding. Resolve it before instrumenting anything, because you cannot reconcile against a rule that does not exist.
Refunding the fee without a charge refund
The application fee can be refunded independently of the charge, and there are legitimate reasons to do so: a promotional fee waiver applied after the fact, a correction to a misconfigured fee percentage, or a goodwill gesture to a seller on a disputed order.
stripe.applicationFees.createRefund does this directly. It has no effect on the buyer, who keeps their goods and their payment — the money moves from your platform to the connected account.
This matters for reconciliation because it breaks the assumption that a fee refund implies a charge refund. A detector that infers "fee refunded, therefore charge refunded, therefore the transfer should have been reversed" will raise a finding on every one of these and be wrong every time.
The reliable direction is to reason from the charge outward, never from the fee inward.
// Refunds the platform's cut to the seller.
// The buyer is unaffected — this is not a charge refund.
await stripe.applicationFees.createRefund('fee_123', {
amount: 1000,
metadata: { reason: 'promotional_fee_waiver' },
});Reconciling against your published terms
The hardest part of this detector is not the arithmetic — it is that the correct behaviour is a policy question, and most platforms have never written the policy down.
A useful exercise before instrumenting anything: take ten recently refunded charges across different amounts and refund types, and check what actually happened to the fee on each. Platforms frequently discover the behaviour is inconsistent — full refunds return the fee because someone set the flag in that code path, partial refunds do not because a different code path was written a year later by someone else.
Inconsistency is worse than either policy, because it means neither your terms nor your books describe what your system does. Sellers on identical transactions get different outcomes, which surfaces eventually as a support pattern nobody can explain.
Once the intended behaviour is written down, reconciliation has something to measure against. Until then, every finding is ambiguous — a shortfall against a rule that does not exist is not a shortfall, it is a question.
Detecting it without double-counting
A detector that checks the fee in isolation will flag charges that are already reconciled through a transfer reversal. If it then acts automatically, it double-refunds.
The correct sequence is: compute the net position across both levers first, and only then attribute any remaining shortfall to a specific cause. Where a transfer reversal has already made the platform whole, there is no finding to raise regardless of what the fee column says.
const netMargin =
(fee.amount - fee.amount_refunded) -
(transfer.amount - transfer.amount_reversed);
if (netMargin >= 0) return null; // already whole — not a finding
const recoverable = Math.min(feeShortfall, -netMargin);