Chapter 6 · The flags-and-defaults matrix
The Platform Refund Ledger · 7 min read
This chapter answers one question: for every combination of refund parameters and charge pattern, who ends up paying what?
Chapters 2 through 5 walked the paths one at a time. This chapter compresses them into the truth table the whole book exists for — first under Stripe's defaults, then under deliberate overrides. Every cell follows from mechanics already cited; the numbers are the book's running examples so you can re-add any cell yourself.
The two parameters, and where they mean anything
At the API level both refund parameters default to false everywhere (docs.stripe.com/api/refunds/create): refund_application_fee ("An application fee can be refunded only by the application that created the charge") and reverse_transfer ("The transfer will be reversed proportionally to the amount being refunded"). The guides then layer pattern-specific behavior on top (docs.stripe.com/connect/destination-charges, /direct-charges, /separate-charges-and-transfers):
reverse_transfer | refund_application_fee | |
|---|---|---|
| Direct | nothing to reverse — no linked transfer exists | meaningful; default keeps your fee |
| Destination | meaningful; default leaves funds with the connected account | meaningful; default keeps your fee; if used, must be paired with reversal |
| Separate | nothing attached to reverse automatically | no fee object exists; nothing to act on |
The matrix under defaults
Running examples per column — direct: $120.00 charge, $12.00 fee; destination: $120.00 charge, $12.00 fee, transfer.amount $108.00 (the fee is withheld from the transfer, chapter 1); separate: $120.00 charge, $108.00 transferred, no fee object. "Platform" means lifetime cash position on that sale.
| Event (defaults, no flags) | Direct | Destination | Separate |
|---|---|---|---|
| Full refund | seller −$120; platform +$12 | platform −$120 against the $12 withheld; seller keeps the $108 transferred; platform −$108 | platform −$120; seller keeps $108; platform −$108 |
| Partial refund $30 | seller −$30; platform +$12 (take rate on remainder: 12 ÷ 90 ≈ 13.3%) | platform −$30; seller untouched; platform −$18 | platform −$30; seller untouched; platform −$18 |
| Several partials totaling $120 | as above, cumulative | platform −$120 across events; −$108 | platform −$120 across events; −$108, spread over objects nothing links |
| Dispute lost ($860-scale, ch. 5) | seller debited amount; fee payer per account config | platform −amount −fee; seller untouched; recovery manual | platform −amount −fee; recovery manual and unlinkable |
Read row two carefully: on both destination and separate patterns, an unflagged partial refund costs the platform exactly its face value while the seller's economics do not move. On the direct pattern the seller absorbs it and the platform's effective take rate silently rises. Same refund call, three different parties harmed.
And read the last row's third cell against the second: identical money outcomes, but the destination case leaves evidence (charge.transfer, amount_reversed) that an audit can join, while the separate case leaves nothing — the transfer was always just a transfer.
[DIAGRAM: The default flow of a refund, per pattern]
Single input: "Refund created". Three lanes.
Lane DIRECT: box "debit connected balance" -> box "platform fee
stays unless refund_application_fee:true" -> exit label "+fee".
Lane DESTINATION: diamond "reverse_transfer?" --false--> box "platform
balance covers refund" -> exit "-charge +fee"; --true--> box "proportional
reversal created" -> diamond "refund_application_fee?" -> exits
"-fee" or "+fee kept".
Lane SEPARATE: box "platform balance covers refund" -> box "(no linkage;
no fee object)" -> exit "-charge, seller untouched".
Caption: only one diamond in this diagram exists by default -- and it
is set to the expensive branch.
The matrix under overrides
Now the deliberate settings. Each row is achievable today with documented parameters; outcomes use the same running examples (full-refund row shown; proportions scale linearly until rounding, which the next section handles).
| Setting | Outcome on full refund | Platform position |
|---|---|---|
Direct + refund_application_fee: true | seller −$120, fee returns to them | $0 (processing cost sunk) |
Destination + reverse_transfer: true only | platform −$120, reversal +$108, fee kept | $0; seller returns to exactly $0, having given back only what they received |
| Destination + both flags true | platform −$120, reversal +$108, fee returned −$12 | −$12; seller ends +$12 — the fee refund over-compensates them |
Destination + refund_application_fee: true alone | prohibited by pairing rule | Stripe requires the reversal alongside |
| Separate + standalone reversal afterward | same movements as above, two steps | $0, delayed by your ops latency |
| Separate + reduce next transfer | seller's next payout smaller by $108 | $0, needs a contractual basis |
The pattern to notice: every good outcome requires naming a party to pay. The defaults decline to name anyone, so the platform pays by omission.
The middle two rows deserve to be read against each other, because the habit of setting both flags together is the most expensive reflex in this table. Because the fee was already withheld from the transfer, a proportional reversal is self-balancing: −$120 out, +$108 back, $12 never sent, and both parties land on exactly $0. Adding refund_application_fee: true on top then hands the seller a $12 fee refund compensating them for a deduction the reversal already unwound — so the platform finishes at −$12 and the seller at +$12 on a sale that no longer exists. The flag is not wrong; it is a deliberate act of compensating the seller, and it belongs only where you mean to.
Where the arithmetic develops slack: multiple partials
Proportional legs are rounded per refund, independently. On a $100.00 destination charge priced with a $10.00 fee — so transfer.amount is 9000 — refunded in thirds of $33.33, $33.33, $33.34:
- Transfer reversals (with
reverse_transfer: true): round(3333 ÷ 10000 × 9000) = 3000, round(3333 ÷ 10000 × 9000) = 3000, round(3334 ÷ 10000 × 9000) = 3001. Total 9001 against the single-shot figure of round(10000 ÷ 10000 × 9000) = 9000. One minor unit reversed too much. - Fee shares: round(3333 ÷ 10000 × 1000) = 333, round(…) = 333, round(3334 ÷ 10000 × 1000) = 333. Total 999 against the single-shot figure of round(10000 ÷ 10000 × 1000) = 1000. One minor unit returned too little.
Neither leg is exact, and the two drift in opposite directions, because each rounds a ratio against a base that is not proportional to itself — the transfer is the charge less the fee, not the charge. The slack is genuine and unattributable to anyone.
Two consequences for anyone auditing (chapter 8 builds on this): compare summed expectations per charge, never per refund alone, and allow a tolerance of about two minor units before calling anything a finding. A check without tolerance reports noise forever; a check with tolerance finds the real gaps, which are rarely about cents.
How to read your own platform through the matrix
Three signatures cover nearly everything seen in practice:
- Destination, defaults, any volume of refunds: platform lifetime position trends toward minus (refunds × average seller share). Visible in data: transfers with
amount_reversed= 0 on refunded charges. - Direct, defaults, sellers who churn: negative connected-account balances (chapter 3's endgame), support load, and write-offs.
- Separate, anything: the same losses as signature 1 with no fields to join on — recoverable only from your own order records.
If your matrix differs from these cells — say, refunds that leave sellers whole while the platform profits — you have either found a policy worth documenting or a bug worth fixing. Chapter 10 turns whichever it is into an explicit decision.
What to check on your own platform
- Fill in this chapter's defaults matrix with your own last month of refunds: count refunds per cell, not dollars yet.
- For each refund path in your code, record which cell it places refunds into.
- Compute your effective take rate on partially refunded charges — expected fee share versus fee kept — and look for silent increases like the 13.3% case above.
- Verify no code path passes
refund_application_fee: trueon a destination charge without also reversing the transfer. - Sum per-charge expected versus actual reversals with a two-unit tolerance; list every charge outside it.
- If you run multiple patterns, confirm each has its own refund path rather than sharing one that assumes a cell.
Sources: docs.stripe.com/api/refunds/create · docs.stripe.com/connect/destination-charges · docs.stripe.com/connect/direct-charges · docs.stripe.com/connect/separate-charges-and-transfers