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.

A lost $600 dispute on a destination charge
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. 1

    Export all charge.refunded events for the last 90 days.

  2. 2

    For each charge that has a transfer, retrieve the transfer and sum reversals.

  3. 3

    Calculate expected reversal = (amount_refunded / amount) × transfer.amount. Flag if actual < expected.

  4. 4

    For every application_fee on those charges, confirm amount_refunded is proportional.

  5. 5

    Pull all charge.dispute.closed with status=lost; verify transfer reversal + fee refund occurred.

  6. 6

    Sum missing amounts. That number is your recoverable baseline.

  7. 7

    Connect FeeGuard if you would rather this ran continuously than once.