Free audit · Fintech CFO / Finance Lead
Dispute clawback: recovering a lost chargeback from a seller
A buyer disputes a $600 order and wins. Stripe debits your platform for $600 plus a $15 dispute fee. The seller who received $540 of it is untouched. Unless you reverse that transfer, you have funded a chargeback on a sale you took $60 from.
Who carries the liability
It depends on the charge type, and the distinction is worth getting right before you build any recovery process.
Destination charges and separate charges & transfers: the platform is the merchant of record. Disputes are debited from the platform balance. This is where clawback matters.
Direct charges: the connected account is the merchant of record, and Stripe debits them directly. There is nothing for the platform to recover — the loss never reached you.
If you are unsure which you are running, check whether your charges carry a transfer_data[destination] or an on_behalf_of. Destination charges live on your account; direct charges do not.
- Original charge
- $600.00
- Your application fee
- $60.00
- Transferred to seller
- $540.00
- Disputed amount debited from you
- −$600.00
- Dispute fee (not recoverable)
- −$15.00
- Your position before clawback
- −$555.00
- Recoverable from seller
- $540.00
Calculating what is genuinely recoverable
The recoverable figure is the portion of the original transfer still sitting with the connected account: transfer.amount − transfer.amount_reversed.
It is not the charge amount, and this trips up most home-grown scripts. The transfer to a connected account is the charge minus your application fee, so using the charge amount overstates the recoverable balance by exactly your own fee — and a reversal for that inflated figure is rejected by Stripe for exceeding the transfer.
The dispute fee is a platform cost. It cannot be passed to the connected account, and including it in a recovery target guarantees a failed reversal.
const transfer = await stripe.transfers.retrieve(charge.transfer as string);
const recoverable = transfer.amount - (transfer.amount_reversed ?? 0);
await stripe.transfers.createReversal(
transfer.id,
{ amount: recoverable },
{ idempotencyKey: `dispute-clawback-${dispute.id}` },
);Act on dispute.created, not dispute.closed
By the time charge.dispute.closed arrives with status: lost, weeks have usually passed and the connected account has been paid out several times over. A reversal then leaves them negative, and recovery depends entirely on whether they keep transacting.
charge.dispute.created is the event worth building a process around. The funds are still at risk rather than already gone, and reversing against a balance that has not yet paid out is far more likely to succeed.
The trade-off is real: reversing early penalises a seller on a dispute that may still be won. Most platforms hold rather than reverse on created, and treat it as a watch-list. Fraud-reason disputes are the exception — they are rarely won, so an early reversal is closer to inevitable than premature.
Always use an idempotency key
A transfer reversal cannot itself be reversed. Undoing a double-reversal means creating a fresh transfer back to the connected account, which is a manual process and an awkward conversation with a seller.
Deriving the key from something stable — the dispute id — means a retried job returns Stripe's original response instead of reversing twice. Note that Stripe idempotency keys expire after 24 hours, so a retry after that window genuinely re-attempts.
What FeeGuard does about it
FeeGuard raises a finding on charge.dispute.created as an early warning and on charge.dispute.closed when the status is lost. The reported figure is the genuinely recoverable balance, not the disputed amount, so a one-click recovery cannot fail for exceeding the transfer.
Open disputes are excluded from automated recovery by design — a seller should not be debited for a dispute that may still be won.
Run the check yourself
Seven steps, no signup. If the number at step 6 is greater than zero, you have a leak — and you did not need us to prove it.
- 1
Export all charge.refunded events for the last 90 days.
- 2
For each charge that has a transfer, retrieve the transfer and sum reversals.
- 3
Calculate expected reversal = (amount_refunded / amount) × transfer.amount. Flag if actual < expected.
- 4
For every application_fee on those charges, confirm amount_refunded is proportional.
- 5
Pull all charge.dispute.closed with status=lost; verify transfer reversal + fee refund occurred.
- 6
Sum missing amounts. That number is your recoverable baseline.
- 7
Connect FeeGuard if you would rather this ran continuously than once.