All docs

Dispute clawback — recovering from connected accounts

A practical procedure for recovering a lost Connect dispute from the connected account that received the funds — and why the timing changes everything.

The recovery path

Create a transfer reversal for the outstanding portion of the original transfer. This is the only mechanism that moves funds back from a connected account.

The amount is capped at transfer.amount − transfer.amount_reversed. Requesting more is rejected.

const transfer = await stripe.transfers.retrieve('tr_123');
const outstanding = transfer.amount - (transfer.amount_reversed ?? 0);

await stripe.transfers.createReversal(
  'tr_123',
  { amount: outstanding },
  { idempotencyKey: 'dispute-clawback-dp_123' },
);

Always use an idempotency key

A reversal cannot itself be reversed. Undoing a double-reversal means creating a fresh transfer back to the connected account — a manual, awkward conversation.

Deriving the idempotency key from something stable (the dispute or discrepancy id) means a retried job returns the original response instead of reversing twice.

When recovery fails

Insufficient funds means the connected account has already paid out. The reversal can still be created — Stripe recovers it from their next inbound volume — but if they never transact again, the platform absorbs it.

This is why acting on charge.dispute.created rather than charge.dispute.closed materially changes recovery rates.

Calculating the right amount

The recoverable figure is transfer.amount − transfer.amount_reversed — what the connected account still holds.

It is not the disputed amount, and this is where most home-grown recovery scripts fail. The transfer is the charge minus your application fee, so using the charge or dispute amount overstates the target by exactly your own fee, and Stripe rejects the reversal for exceeding the transfer.

The dispute fee is a platform cost and cannot be included either. A reversal request built from dispute.amount + dispute_fee will fail on every single dispute.

When recovery fails

Insufficient funds. The connected account has already paid out. The reversal can still be created and Stripe recovers it from their next inbound volume — but if they never transact again, you absorb it. This is the most common failure and the one timing solves.

Already reversed. Someone reversed it manually in the interim. Not a failure; close the finding and move on.

Missing permission. The API key lacks transfers:write. A configuration problem, not a recovery problem.

No such transfer. Usually means the event was attributed to the wrong Stripe account, or the charge was a direct charge with no platform-side transfer at all.

Where the money actually comes from

A reversal does not pull funds out of a bank account. It debits the connected account's Stripe balance, which is a different thing with different failure characteristics.

If they have a positive balance, the reversal settles immediately and invisibly. If they do not, the balance goes negative and Stripe recovers it from their next inbound volume — which may be tomorrow, next month, or never.

A reversal is therefore best understood as a claim on future volume rather than a retrieval of past funds. That framing explains why timing dominates outcomes: you are competing with the seller's payout schedule, and payouts run daily on most accounts.

Recovering across several transfers

On separate charges and transfers, a single disputed order may have been funded by more than one transfer — split shipments, staged fulfilment, or a marketplace order fulfilled by multiple sellers.

There is no single transfer to reverse. Recovery means identifying every transfer in the transfer_group, computing each one's share of the disputed amount, and reversing proportionally.

Each reversal needs its own idempotency key. Reusing one key across several reversals means the second and subsequent calls return the first reversal's response, and you will believe you recovered the full amount while having recovered only a fraction of it.

const transfers = await stripe.transfers.list({
  transfer_group: 'order_456',
});

const total = transfers.data.reduce((sum, t) => sum + t.amount, 0);

for (const transfer of transfers.data) {
  const share = Math.round((transfer.amount / total) * disputedAmount);
  const outstanding = transfer.amount - (transfer.amount_reversed ?? 0);

  await stripe.transfers.createReversal(
    transfer.id,
    { amount: Math.min(share, outstanding) },
    { idempotencyKey: `clawback-${dispute.id}-${transfer.id}` },
  );
}

Deciding whether to recover at all

Not every recoverable dispute should be recovered, and a process that reverses unconditionally will cost you sellers.

Amount relative to the relationship. Reversing $40 from a seller who generates six figures a year is rarely worth the conversation. Reversing $4,000 always is. Most platforms set a floor.

Fault. A dispute caused by a seller shipping nothing is different from one caused by genuine card fraud on your checkout. Reversing the second penalises a seller for a failure that was yours.

Pattern. A first dispute from a long-standing seller is noise. A cluster from a new account is a signal, and recovery there is often secondary to offboarding them.

These are business rules, not technical ones, and they belong in a policy your team can point at rather than in individual judgement calls made under time pressure. Automate the detection and the arithmetic; keep the decision explicit.

Communicating a clawback to the seller

This is the part most platforms underinvest in, and it is where the relationship damage happens rather than in the reversal itself.

A seller who sees an unexplained debit assumes an error and opens a support ticket. A seller who received a note naming the order, the dispute reason, and the amount generally accepts it — the money was never really theirs.

Worth putting in your terms before you need it: state that disputed transactions may be reversed from connected account balances. Recovering funds under a term the seller agreed to is administration. Recovering them under no term at all is a dispute of your own.

Monitoring recovery rate

Two numbers tell you whether your process works: the share of lost disputes where a reversal was attempted, and the share of those where funds actually arrived.

A high attempt rate with a low success rate means you are acting too late — the sellers have already been paid out. That is a timing fix, not a process fix, and it usually means moving from charge.dispute.closed to charge.dispute.created.

A low attempt rate means findings are not reaching anyone who acts on them, which is an alerting problem.