Skip to content
All posts

Partial refunds and proportional reversal

A partial refund does not proportionally reverse anything by default. You choose the refund amount, the reversal amount and the fee refund independently, and they can disagree.

FeeGuard6 min read
refundsarithmetic

What "partial" leaves undecided

A full refund has an obvious correct answer, even if your code does not implement it: everything goes back. A partial refund has three numbers, and Stripe will let you set them independently.

  • The refund amount. How much the buyer gets back.
  • The reversal amount. How much comes back out of the connected account.
  • The fee refund amount. How much of your application fee you give up.

Nothing enforces a relationship between them. You can refund 40% of a charge, reverse 100% of the transfer and refund none of the fee, and every one of those calls will succeed. The platform absorbs whatever you do not explicitly allocate, and it absorbs it silently.

The mechanism behind this — three separate objects with three separate lifecycles — is covered in the refund that only costs the platform. What follows is the arithmetic.

Computing the reversal

The proportional rule

The default expectation on both sides of a marketplace is that a partial refund preserves the original split. If your take rate was 12%, it should still be 12% on whatever value remains.

Work in minor units as integers, always. Money in a floating-point number is a bug waiting for a large enough transaction.

// All amounts in minor units, as Stripe represents them.
const chargeAmount   = 240000;  // 2,400.00
const applicationFee =  28800;  //   288.00  (12%)
const transferAmount = 211200;  // 2,112.00

const refundAmount   =  90000;  //   900.00 partial refund

const proportion = refundAmount / chargeAmount;            // 0.375
const reversal   = Math.round(transferAmount * proportion); // 79200
const feeRefund  = Math.round(applicationFee * proportion); //  10800

Refund 900, reverse 792, give back 108 of your fee. The seller keeps 1,320, you keep 180, and the split is still 12%.

Rounding, and who absorbs the remainder

Math.round is doing something consequential there. Proportions rarely divide cleanly, and each of the two roundings can go either way, so the three numbers will not always sum exactly to the refund.

Stripe computes its own proportional reversal when you pass reverse_transfer: true without an explicit amount, and it does so independently of whatever you computed. That means your ledger and Stripe's can legitimately differ by a minor unit or two on the same transaction. This is normal. Reporting it as a loss is how a monitoring tool teaches people to ignore it — a sensible detector treats anything at or below a couple of minor units as fully reconciled, and only reports a shortfall once it is large enough to be a real recoverable amount rather than a rounding artifact.

Decide once who absorbs the remainder, write it down, and be consistent. In practice the platform absorbing a one-cent difference is the only answer that does not require a conversation.

Multiple partials against one charge

This is where it stops being arithmetic and starts being state.

A charge can be refunded several times. Each refund is proportional to the original charge, not to what remains, and each reversal has to be checked against the total already reversed rather than against zero.

const alreadyReversed = transfer.amount_reversed;               // 79200
const targetReversed  = Math.round(transferAmount * totalRefundedProportion);
const thisReversal    = Math.max(0, targetReversed - alreadyReversed);

Computing each reversal in isolation is how platforms end up reversing more than they transferred, which fails — or worse, refunding more application fee than they ever collected. That second one is a genuine signature worth watching for: if the total fee refunded exceeds the fee originally charged, something has been applied twice, and it is almost always a retry without an idempotency key.

Currencies that have no cents

The proportional rule assumes there is a smaller unit to absorb the remainder. For several currencies there is not.

Stripe treats JPY, KRW, VND, CLP and around a dozen others as zero-decimal: the amount is the whole unit, and there is no fraction to round into. A three-decimal currency such as BHD, KWD or JOD goes the other way. If your refund maths divides by 100 to display or compute anything, it is wrong by a factor of a hundred for the first group and by ten for the second — and it will be wrong quietly, on a small share of your volume, for as long as nobody checks.

Compute in minor units, use the currency's own exponent for display only, and never let a division into major units enter the calculation.

Refunds are easy. Getting the money back from the connected account is the hard part.

Where the drift accumulates

The reason partial refunds deserve their own post is that each individual error is too small to notice and the mechanism that produces them does not self-correct.

A full refund with no reversal is a large, visible discrepancy — the whole transfer. A partial refund with a slightly wrong reversal is a fraction of a fraction, on one transaction, and it looks exactly like rounding. Multiply it by every partial refund your platform issues, across a year, with a code path nobody has revisited since it was written, and the aggregate is real while no single row ever looked worth investigating.

The check is cheap. Take your charges with more than one refund against them, compute the proportion refunded, compare it to the proportion reversed, and look at the distribution. If the two track each other within a minor unit or two, your partial path is correct. If there is a systematic gap in one direction, you have found a bug that has been running for as long as the code has.

Common questions

Should the split always be proportional?

Not necessarily, and this is a commercial decision rather than a technical one. Some platforms deduct a fixed handling charge from any refund. Some make the seller absorb the whole of a partial refund if it was their fault. Both are defensible, and both are fine as long as your code, your seller agreement and your invoices agree. The failure is not choosing a non-proportional split — it is having no policy and letting the default decide per transaction.

What if the connected account cannot cover the reversal?

The account goes negative and the deficit is recovered from their future earnings. If they stop trading before that happens, the platform generally ends up carrying it. This is the main reason the age of a discrepancy matters more than its size — the same reversal is routine on the day of the refund and a bad debt three months later.

Does refund_application_fee support a partial amount?

The fee refund can be made for a specific amount rather than the whole fee, which is what makes a proportional split expressible. If you pass the flag without an amount you give the whole fee back, which on a partial refund is usually more generous than you intended.

Partial refund transfer math on Stripe Connect

FeeGuard is an independent product and is not affiliated with, endorsed by, or sponsored by Stripe, Inc. "Stripe" and "Stripe Connect" are trademarks of Stripe, Inc., referenced descriptively.