Chapter 2 · Refunds: every path
The Platform Refund Ledger · 7 min read
This chapter answers one question: when you call the Refunds API on each charge pattern, exactly which balances move, and by how much?
A refund is one API object that can trigger different sets of movements depending on two parameters and the charge's pattern. The customer-facing outcome is always the same. The platform-and-seller outcome is decided here.
The Refund object itself
Creating a refund (POST /v1/refunds) returns an object with these load-bearing fields (docs.stripe.com/api/refunds/create):
| Field | Meaning |
|---|---|
amount | How much was refunded, in the smallest currency unit. Defaults to the entire unrefunded amount of the charge. |
charge | The charge refunded. You may instead pass a payment_intent. |
status | pending, succeeded, failed, or canceled. |
reason | Optional: duplicate, fraudulent, or requested_by_customer. |
balance_transaction | The ledger line this refund created on the refunding account. |
transfer_reversal | Populated when the refund also reversed a transfer — proof of what chapter 3 covers. |
refund_application_fee | Request parameter: whether the application fee is returned. Full charge refund → full fee; otherwise proportional. |
reverse_transfer | Request parameter: whether the linked transfer is reversed. Proportional to the refund either way. |
Two rules from the same page frame everything: you can refund partially as many times as you like until the charge is exhausted, and once entirely refunded, a charge cannot be refunded again.
Pattern 1 — Direct charges: the seller's money
On a direct charge the funds were never yours; they settled in the connected account's balance. The refund comes out of that balance. Stripe's direct-charges documentation states the fee rule plainly: "Application fees aren't automatically refunded when issuing a refund. Your platform must explicitly refund the application fee or the connected account … loses that amount" (docs.stripe.com/connect/direct-charges).
Default outcome, worked: a $120.00 direct charge with a $12.00 application fee is refunded in full.
| Leg | Account | Amount |
|---|---|---|
| Refund debit | connected account balance | −$120.00 |
| Application fee | stays on platform balance | +$12.00 kept |
The platform's lifetime position on this sale: +$12.00. The seller gave back everything including your fee. If the seller's balance was already paid out, their balance simply goes negative — chapter 3 covers what happens then. Passing refund_application_fee: true moves the fee back to the seller instead, leaving the platform at $0.00 (the processing cost already paid is gone either way).
Pattern 2 — Destination charges: your money first
Stripe's destination-charges documentation describes the default with unusual clarity: "When refunding a charge that has a transfer_data[destination], by default the destination account keeps the funds that were transferred to it, leaving the platform account to cover the negative balance from the refund" (docs.stripe.com/connect/destination-charges). To pull funds back, set reverse_transfer: true; if the whole charge is refunded the entire transfer is reversed, otherwise a proportional amount is.
Same $120 sale, priced with application_fee_amount of $12 (so transfer.amount = $108 — the fee is withheld from the transfer, see chapter 1), refunded in full under both flag settings:
| Leg | Defaults (no flags) | Both flags true |
|---|---|---|
| Position after the sale (fee withheld from the transfer) | +$12.00 | +$12.00 |
| Refund debit | −$120.00 platform | −$120.00 platform |
| Transfer reversal | none | +$108.00 platform |
| Application fee | kept | −$12.00 returned to seller |
| Platform position on the sale | −$108.00 | −$12.00 |
The left column deserves a slow read: every API call succeeded, the customer is made whole, and the platform is down $108.00 — precisely the seller's net — on a transaction that no longer exists. The right column carries a quieter surprise: reversing the transfer already returns both parties to zero, so returning the fee on top of it leaves the platform down exactly $12.00 and the seller up exactly $12.00. Chapter 6 lays that trap out in full. One documented constraint belongs here too: "If you refund the application fee for a destination charge, you must also reverse the transfer" (same page). The fee return is not meaningful without the movement it compensates.
Pattern 3 — Separate charges and transfers: nothing is linked
Stripe's separate-charges documentation: "refunding a charge has no impact on any associated transfers. It's up to your platform to reconcile any amount owed back to it by reducing subsequent transfer amounts or by reversing transfers" (docs.stripe.com/connect/separate-charges-and-transfers).
There is no reverse_transfer linkage to rely on and usually no ApplicationFee object for refund_application_fee to act on (chapter 1). The refund debits your platform balance for the refund amount; the transfer sits with the seller until you do something explicit — reverse it (chapter 3), or deduct from a future transfer.
Worked: the $120 separate-pattern sale ($108 transferred), refunded in full with nothing further done: platform −$120.00, seller keeps $108.00. Platform position: −$108.00, identical headline to the destination default — but with no field anywhere in Stripe's data model that would surface it, because nothing ever linked the charge to the transfer.
[DIAGRAM: Where a $120 full refund lands, by pattern]
One input node: "POST /v1/refunds {charge: ch_…}".
Three branches:
DIRECT --> arrow labeled "debit" --> [Connected acct −120]
dashed arrow labeled "+12 stays" --> [Platform]
DESTINATION (defaults) --> "debit" --> [Platform −120]
dotted arrow "seller keeps net 108" --> [Connected acct]
DESTINATION (reverse_transfer:true) --> "debit" --> [Platform −120] and
"reversal +108" returning arrow from [Connected acct],
plus "−fee to seller" only if refund_application_fee:true
SEPARATE --> "debit" --> [Platform −120]
dotted arrow "108 untouched" --> [Connected acct]
Caption: identical customer outcome in all four rows; four different
platform outcomes (+12, −108, 0 or −12, −108).
Partial refunds: three numbers you set independently
A partial refund of amount R creates up to three movements whose sizes are controlled separately: the refund itself (R), the transfer reversal, and the fee refund. When reverse_transfer or refund_application_fee is set, Stripe computes those legs proportionally — the API reference's wording: the transfer "will be reversed proportionally to the amount being refunded," and the fee "will be refunded in an amount proportional to the amount of the charge refunded" (docs.stripe.com/api/refunds/create).
Worked proportions on the $120 destination charge (transfer.amount $108.00): refund R = $30.00 (25%) with both flags:
- Reversal: round(0.25 × 108.00) = $27.00
- Fee returned: round(0.25 × 12.00) = $3.00
- Platform cash flow for the episode: −30.00 + 27.00 − 3.00 = −$6.00 — the $3.00 of fee it handed back, plus the $3.00 of refund the reversal could not reach because the transfer was smaller than the charge by exactly the fee.
Leave the flags off and the same $30 refund costs the platform $30.00 while the seller keeps everything. The failure mode is not choosing wrong between these; it is having only one path, which makes the choice for every refund forever.
What to check on your own platform
- Read the actual refund-creation code paths (including Dashboard-created refunds from support staff) and record, per path, what
reverse_transferandrefund_application_feeare set to. - Pick five real refunds from the last month and confirm, from the
Refundobjects, whether atransfer_reversalid is present where your policy says one should be. - On any direct-charge flow, verify someone consciously chose the fee behavior rather than inheriting the keep-by-default rule.
- If you use separate charges and transfers, write down where the "reduce a future transfer" logic lives, or record honestly that it does not exist.
- Check
charge.refundedandcharge.amount_refundedfor charges with multiple partials — the per-refund arithmetic in chapter 6 depends on summing, not averaging.
Sources: docs.stripe.com/api/refunds/create · docs.stripe.com/connect/direct-charges · docs.stripe.com/connect/destination-charges · docs.stripe.com/connect/separate-charges-and-transfers