The complete anatomy of a Stripe Connect refund
One refund moves several objects and debits a different account depending on charge type. This piece maps every hop for all three Connect charge types.
The complete anatomy of a Stripe Connect refund
A single refund can move five different API objects and debit either the platform balance or a connected account's balance, depending on the Connect charge type behind it. This piece walks one $100.00 refund through direct charges, destination charges, and separate charges and transfers, so platform engineers and finance ops can predict every hop before issuing it.
The objects inside one refund
A refund is one API call, but it is never one object. Issuing one writes a new Refund, updates the Charge, updates the wrapping PaymentIntent, and — depending on parameters and charge type — reaches into Transfer and ApplicationFee children as well. Fix the cast before tracing balances:
- Charge — the record of the original payment, carrying
amount,amount_refunded, and the booleanrefundedonce nothing is left to return. - Refund — the payback record:
amount,status, and thebalance_transactiondescribing the debit. - PaymentIntent — the lifecycle wrapper around the charge attempt; it tracks cumulative refunds and is the handle most integrations pass to the Refunds API.
- Transfer — funds moved from the platform to a connected account. Exists only in the destination and separate patterns.
- TransferReversal — a child of a Transfer that pulls some or all of those funds back to the platform.
- ApplicationFee — the platform's fee on a direct or destination charge, held as its own object with
amountandamount_refunded. - ApplicationFeeRefund — a child of the ApplicationFee, created when the fee is returned.
Where each object sits depends on the charge type. Direct charges are created on the connected account through the Stripe-Account header, so the Charge, Refund, and PaymentIntent live there. Destination charges are created on the platform with transfer_data[destination], and separate charges pair a platform Charge with decoupled Transfers, so those objects sit on the platform. The table below maps each object to its owning account side and states whether a default refund call touches it.
| Object | Lives on | Touched by a default refund? |
|---|---|---|
| Charge | Connected account (direct); platform (destination, separate) | Yes, marked refunded |
| Refund | Same account as the Charge | Created by the call itself |
| PaymentIntent | Same account as the Charge | Updated with refund totals |
| Transfer | Platform (destination, separate only) | No |
| TransferReversal | Platform, child of the Transfer | No |
| ApplicationFee | Platform-side record tied to the connected account's charge | No |
| ApplicationFeeRefund | Platform, child of the ApplicationFee | No |
Two absences in that table matter as much as the rows. A direct charge has no Transfer, so there is nothing to reverse. Separate charges and transfers create no ApplicationFee objects, because the platform collects its cut by transferring less. A default refund therefore moves exactly three objects — Charge, Refund, PaymentIntent — and everything else waits for explicit flags or follow-up calls (Refunds API).
The objects also reference each other, which is what makes programmatic auditing possible. A Refund points at its charge and usually its payment_intent. An ApplicationFee carries charge and account. A Transfer created by transfer_data traces back to the originating charge, and every TransferReversal carries a source_refund when a refund caused it. Follow those pointers and a single refund ID expands into the entire money trail.
Who funds the buyer
The buyer is always made whole against the original payment method. The open question is whose Stripe balance funds the payback and what happens when that balance runs short. The table below summarizes debit behavior per charge type; the paragraphs after it expand on the edge cases.
| Charge type | Balance debited on refund | If that balance is short |
|---|---|---|
| Direct | Connected account's available balance | Refund enters status pending and processes automatically once funded |
| Destination | Platform balance | Pending or failed; with a reversal attached and a depleted connected account, the API returns an error instead of creating a pending refund |
| Separate | Platform balance | Pending or failed; transfers are never touched |
Three facts drive the table. First, refunds draw only on the available balance — money still sitting in pending cannot fund them, which is why payout timing (charges land in pending and become available on a rolling schedule) shapes what a refund can do (refunds, payouts). Second, Stripe states plainly that "Stripe's processing fees from the original transaction aren't returned," which is why every ledger below retains the processing cost somewhere. Third, the debit target differs by construction: Stripe debits the connected account directly for direct charges, and debits the platform balance for destination and separate charges (connect charges compared).
The direct-charge shortfall path is patient. If the connected account's available balance cannot cover the refund, Stripe creates the refund anyway with status pending and processes it automatically once payouts or new payments fund the account.
The destination path is strict when a reversal rides along. A plain destination refund simply debits the platform. But if the request includes reverse_transfer=true and the connected account lacks the funds to give back, Stripe returns an error rather than creating a pending refund. That asymmetry is useful: you learn at refund time that recovery is impossible, instead of discovering a pending refund that will quietly strand the money with the seller.
Separate charges are the indifferent case: refunding the charge has no effect on any transfer, ever. Recovery is a separate, manual act covered below.
One configuration changes none of this debit logic. With on_behalf_of, settlement moves to the connected account's country and currency, their statement descriptor applies, and country-specific fees kick in — yet for destination and separate charges the platform balance is still debited for refunds and disputes (connect charges compared). Whose descriptor the buyer sees has no bearing on whose balance absorbs the refund.
Failure handling closes the loop. A failed refund returns the funds to your balance, within up to about 30 days, with failure_balance_transaction and failure_reason populated and a refund.failed event fired. Track that balance transaction back into your books so the returned funds are not counted twice. For destination charges, failed or canceled refunds deposit back into the platform balance, since the platform funded them in the first place.
Direct charges, walked through the ledger
Assumptions for every ledger in this piece:
- US platform, USD throughout.
- Stripe's standard US card pricing assumed: 2.9% + $0.30, per Stripe's published pricing.
- Charge of $100.00 (10000 cents) carrying
application_fee_amountof $10.00 (1000 cents). - Stripe's processing fee billed to the connected account, the standard direct-charge arrangement.
The processing fee follows from the assumption: 2.9% of $100.00 is $2.90, plus the $0.30 fixed component, giving $3.20 (320 cents).
Sale. The connected account receives the payment net of the processing fee and your fee:
gross charge $100.00
processing fee -$3.20
application fee to you -$10.00
seller net $86.80
Your platform holds $10.00 of fee income. Conservation check: $86.80 + $10.00 + $3.20 = $100.00.
Full refund, defaults. You call the Refunds API with no flags. Stripe debits the connected account's available balance by the full amount and the application fee stays with you. Stripe's documentation is explicit: "Application fees aren't automatically refunded when issuing a refund. Your platform must explicitly refund the application fee or the connected account—the account on which the charge was created—loses that amount" (direct charges).
seller before $86.80
refund debit -$100.00
seller after -$13.20
your fee kept +$10.00
The seller is now negative by exactly the two costs Stripe does not return: the $10.00 fee you kept and the $3.20 processing fee. Conservation: -$13.20 + $10.00 + $3.20 = $0, with the buyer whole.
Full refund with refund_application_fee=true. The flag pushes the fee back to the connected account:
seller -$13.20
fee refund +$10.00
seller final -$3.20
your position $10.00 - $10.00 = $0.00
The seller now absorbs exactly the non-returned processing fee and you break even. That is the honest floor for a direct-charge platform on a full refund: someone pays Stripe $3.20, and by default it is your seller.
Partial refund of $40.00 with the flag. Proportionality applies to the fee as well. The fee refund is 40% of $10.00:
fee share 0.40 x $10.00 = $4.00
seller -$40.00 + $4.00 = -$36.00
your position $10.00 - $4.00 = $6.00
The seller again eats the unreturned slice of the processing fee, scaled to the refund. Every direct-charge refund, full or partial, is a choice between these two shapes.
Destination charges, walked through the ledger
Keep the same assumptions and add transfer_data[destination] pointing at the seller, with application_fee_amount of 1000 cents (what a destination charge is). The transfer to the seller defaults to the charge amount minus the application fee: $100.00 - $10.00 = $90.00.
Sale. Two platform-side balance transactions appear immediately:
charge net of processing +$96.80 (10000c - 320c)
transfer to seller -$90.00
platform margin $6.80
The seller's connected balance gains $90.00 and Stripe retains $3.20. Conservation: $6.80 + $90.00 + $3.20 = $100.00.
Full refund, defaults. Both flags false. Per Stripe's destination-charge refund documentation, the destination account keeps the transferred funds and the platform covers the refund (destination charges):
margin $6.80
refund -$100.00
platform position -$93.20
seller +$90.00 (unchanged)
Conservation: -$93.20 + $90.00 + $3.20 = $0. This is the default leak: the buyer is made whole, the seller keeps everything, and the platform funds the difference. Partials scale the same mechanics down — with the flag set, Stripe reverses a proportional slice of the transfer rather than the whole thing, computed against the refunded fraction of the charge. Platforms that discover this pattern months late are the reason destination-charge refund accounting deserves its own runbook; FeeGuard's detectors look for exactly this signature — platform-funded refunds with transfers left standing (more on the leak).
Full refund with both flags true. Now reverse_transfer=true and refund_application_fee=true ride together:
margin (from sale) +$6.80
refund -$100.00
transfer reversal +$90.00
platform subtotal -$3.20
application fee refund -$10.00
platform final -$13.20
seller: $90.00 - $90.00 + $10.00 = +$10.00
Conservation: -$13.20 + $10.00 + $3.20 = $0, buyer at zero. Read the seller line twice. The reversal claws back the $90.00, then the fee refund hands $10.00 straight back. Application fee refunds compensate the connected account, never the buyer, so on destination charges the both-flags reflex over-compensates your seller by the full fee on every full refund. Set the flags deliberately, per refund policy, not by habit.
A useful policy test before standardizing: ask who should be $10.00 richer after a fully refunded $100.00 sale. If the answer is nobody, refund_application_fee belongs only in cases where you intend to compensate the seller explicitly, not in your default refund path.
Separate charges and transfers, walked through the ledger
New inputs: the $100.00 charge sits on your platform and you separately transfer $70.00 to the seller. Your margin is $96.80 - $70.00 = $26.80. Stripe's documentation is blunt about refunds: "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" (separate charges and transfers).
Full refund, defaults. Nothing automatic reaches the transfer:
charge net +$96.80
transfer -$70.00
refund -$100.00
platform position -$73.20
seller +$70.00 (untouched)
Conservation: -$73.20 + $70.00 + $3.20 = $0. You are out the buyer's $100.00 minus the processing fee you already retained, and the seller is up $70.00 until you act. The corrective move is a transfer reversal, and it carries a hard precondition: the connected account's available balance must cover the reversal amount or the call fails. If it succeeds, +$70.00 returns and the platform rests at -$73.20 + $70.00 = -$3.20 — the permanent processing-fee residue. Reducing subsequent transfer amounts instead of reversing works too, and sidesteps that balance gate, at the price of running the reconciliation yourself. Note also that when an async payment method fails on this pattern, Stripe does not reverse anything either; every recovery here is manual.
The two flags that decide where the money goes
Everything above reduces to two booleans on the Refunds create call. The table lays out their behavior side by side.
| Parameter | What it does | Proportional? | Who can set it | Destination default | Direct default |
|---|---|---|---|---|---|
refund_application_fee | Returns the application fee, landing back with the connected account | Full refund returns the full fee; partial returns a proportional share | Only the application that created the charge | false | false |
reverse_transfer | Creates a TransferReversal against the original Transfer | "The transfer will be reversed proportionally to the amount being refunded" | Only the application that created the charge | false | false |
Two quotations anchor the restrictions, both from the parameter reference. On fees: "An application fee can be refunded only by the application that created the charge." On transfers: "The transfer will be reversed proportionally to the amount being refunded."
Defaults deserve emphasis: both flags are false everywhere, for every charge type. Silence is the expensive option. Three operational notes follow:
- On destination charges, refunding the fee requires reversing the transfer in the same request; you may instead leave the flag false and refund the fee separately through the Application Fees Refund API afterwards (flag explainer).
- On direct charges,
reverse_transferis inert — no transfer exists to act on (flag explainer). - On separate charges, neither flag finds an object to touch; recovery is always a standalone reversal against the transfer you chose to send.
Treat both flags as policy encoded per request, not global configuration. Teams that standardize them inside one refund service — with the charge type deciding the defaults — stop relitigating this arithmetic every time support issues a refund by hand.
Events and statuses to watch
A refund is asynchronous machinery wearing a synchronous-looking interface, so wire bookkeeping to events rather than HTTP responses. Statuses traverse a small machine: succeeded, pending, failed, canceled, requires_action. Pending means Stripe is waiting — usually on funds or a slow payment method. Failed and canceled mean the payback aborted and the money lands back in your balance. Requires_action means the customer must complete a step before the payment method accepts the refund.
One nuance saves support tickets: a refund issued shortly after the charge may settle as a card-network reversal rather than a standard refund. The signal is destination_details[card][type] reading reversal instead of refund. It is cheaper on the network side and produces no acquirer reference number (refunds).
The table below lists the events relevant to refund-time money movement and what to verify in each handler (event list). Webhook delivery is at-least-once, so handlers should tolerate duplicates.
| Event | Fires when | What to check |
|---|---|---|
refund.created | A refund request is accepted | Amount, currency, metadata tags for reconciliation |
refund.updated | The refund's status changes | Transitions out of pending; age of stuck refunds |
refund.failed | Processing fails; funds return to your balance | failure_reason, failure_balance_transaction, re-issue decision |
charge.refunded | The charge is refunded, partially or fully | Cumulative amount_refunded against your order system (event explainer) |
application_fee.refunded | A fee is refunded by flag or by the fee-refund API | Refunded amount equals the proportional expectation |
transfer.reversed | A transfer is reversed in whole or part | Sum of reversals against the expected proportional amount |
Do not expect transfer.reversed without cause — outside the flagged refund path and async-failure cleanup on destination charges, reversals happen only when you call for them (can Stripe reverse transfers automatically?). Stripe has deprecated the older charge.refund.updated event; migrate anything still listening to refund.updated. For timing-sensitive reconciliation, subscribe to balance.available as well: it marks the moment pending funds become usable, which is exactly when a stuck pending refund becomes fundable.
Frequently asked questions
Does Stripe reverse the transfer automatically?
Only in specific cases. On destination charges, passing reverse_transfer=true makes Stripe create the reversal as part of the refund — the entire transfer for a full refund, a proportional slice for a partial one. When an async payment method fails after a destination charge settles, Stripe reverses the transfer on its own. Separate charges and transfers never see an automatic reversal, and direct charges have no transfer to reverse.
Why did the connected account go negative after a refund?
Because the charge was direct. Refunds on direct charges debit the connected account's available balance, and the application fee stays with you unless you passed refund_application_fee=true. A negative balance pauses payouts until future payments offset it, and Stripe attempts a debit of the account's external bank account only when debit_negative_balances is enabled for supported regions (account balances).
Who receives an application fee refund?
The connected account, always. Refunding a fee pushes the fee funds back to the connected account — it never reroutes money to the buyer. On destination charges this is why refunding the fee without reversing the transfer distorts the ledger: the seller pockets the fee on top of the transfer you failed to claw back.
What if the platform balance cannot cover the refund?
Refunds draw on the available balance only. Card refunds that cannot be funded sit in pending and complete automatically once the balance recovers. Some payment methods cannot pend and fail instead; a failed refund returns the funds to your balance within up to about 30 days, leaving failure_reason and failure_balance_transaction behind so you can decide whether to re-issue.
Run the 90-day audit
Every figure in this piece is reproducible from your own Stripe data: charges, refunds, transfers, reversals, and fee objects joined by ID. FeeGuard runs exactly that join. Point a restricted, read-only API key at your platform and the free audit reads your last 90 days of Connect activity, reporting every unreclaimed application fee, unreversed transfer, and uncovered dispute loss with the amounts attached. You get the complete answer first; ongoing monitoring that catches each new occurrence as it happens is optional afterward.
FeeGuard is an independent product and is not affiliated with, endorsed by, or sponsored by Stripe, Inc.