Recovery playbook

How to reverse a Stripe Connect transfer

A refund issued without reverse_transfer leaves the money sitting in the connected account while your platform balance absorbs the loss. Getting it back is one API call — but the amount has to be computed correctly or Stripe rejects it, the timing matters more than most teams expect, and the call is irreversible in a way very few payment operations are. This playbook covers the whole procedure: when a reversal is possible, the exact call, the exact arithmetic, the failure modes, and what to record afterwards so month-end never re-litigates it.

When you can reverse a transfer

A reversal is possible whenever the transfer exists and still has unreversed balance. Destination charges qualify because the platform created the transfer at charge time; separate charges & transfers qualify for the same reason. Direct charges are the exception: the connected account was always the merchant of record and no platform-side transfer exists to pull back, so there is nothing to reverse and nothing to compute.

The running total of what has already come back lives on the transfer object as amount_reversed. Everything you might still recover is therefore transfer.amount − amount_reversed — not the charge amount, not the disputed amount, and never an amount you remember from a dashboard. Compute from live state every time; cached figures are how over-recovery complaints begin.

The call

Create a reversal against the transfer id. The amount is in minor units and is capped at the unreversed balance — passing more fails immediately. Pass refund_application_fee only if returning the platform fee on this refund is actually your policy; it is never implied by the reversal itself.

await stripe.transfers.createReversal(
  'tr_123',
  { amount: 9000, refund_application_fee: true },
  { idempotencyKey: 'recovery-ch_123' },
);

Getting the amount right

The proportional rule is the whole game: expected reversal = round((amount_refunded ÷ original charge amount) × original transfer amount). Using the charge amount instead of the transfer amount is the single most common implementation error — it overstates the recovery target by exactly your own application fee, and Stripe rejects reversals that exceed the remaining balance. On partial refunds the same formula applies with the refunded share; clamp the ratio at 1 so a data anomaly can never produce a reversal larger than the transfer.

Worked through round numbers so you can check your own script against it:

A $100 charge with a $10 fee, $90 transferred
Full refund → expected reversal
$90.00
Partial refund at 40% → expected reversal
$36.00
Partial refund at 30% → expected reversal
$27.00
If flag was omitted on the 30% refund
$27.00 missing

What can go wrong

Amount exceeds the transfer. You computed from the disputed or charged amount rather than the transfer. The error arrives instantly and the fix is the formula above, not a retry loop.

No idempotency key plus a retry. Both calls succeed and you have reversed twice — and a reversal cannot itself be reversed. Undo means creating a fresh transfer back to the connected account and having a conversation you did not want to have. Derive the key from something stable like the finding or dispute id.

The seller already paid out. The reversal still succeeds, but it drives the connected account negative and your recovery quietly becomes a claim on their future volume. Retrieval has become bookkeeping with worse odds — which is why detection timing dominates everything downstream.

Verify and record

After the call returns, re-fetch the transfer and confirm amount_reversed moved by exactly the amount you sent. Locate the transfer_reversal balance transaction Stripe created — it is your accounting artefact. Then write the finding down with the reversal id attached: what you found, what you computed, what you did, when. Record what you did, not what you intended; month-end reconciliation cannot re-litigate a finding whose evidence chain is complete.

What FeeGuard does about it

FeeGuard computes the reversal amount per finding from live state rather than webhook payloads, so the amount-exceeds-transfer failure does not occur. Every automated clawback carries an idempotency key derived from the discrepancy id, meaning a retried job returns Stripe’s original response instead of reversing twice. On the free tier every action stays a deliberate one-click decision; on Monitor Pro the same mechanics run inside the thresholds you configure, with each action landing in an append-only audit trail.

Common questions

Can I reverse part of a transfer?

Yes. The amount is capped at the unreversed balance and partial reversals are ordinary operations. Many platforms deliberately recover large findings in tranches to soften the seller-side impact.

Does reversing also refund the application fee?

Only if you pass refund_application_fee: true. It is never implied — and if a transfer reversal already made you whole, refunding the fee separately hands the same money back twice. Check the net position first.

Can a reversal be undone?

Not directly. There is no un-reverse API. Undo means creating a fresh transfer back to the connected account and explaining the correction, which is why the idempotency discipline above matters more here than anywhere else in payments code.

How long do I have to act?

There is no Stripe-imposed expiry. The real limit is the connected account’s reachable balance, which erodes with every payout cycle — see the post-payout playbook for what changes once they have been paid.