Unreversed transfer

Find every unreversed Stripe Connect transfer

Every refund issued without `reverse_transfer` costs you the full transfer amount. Nothing errors, nothing logs, and the only symptom is a platform balance lower than your ledger predicts. Here is how to find all of them at once.

The detection, in full

For each refunded charge that has a transfer, compare what should have come back against what actually did. The gap is your recoverable balance.

Two details matter and are usually where home-grown scripts go wrong: the comparison must be proportional (a 30% refund expects a 30% reversal, not a full one), and it must tolerate a cent or two of rounding, because Stripe rounds each partial reversal independently of your calculation.

A single $100 refund issued without reverse_transfer
Charge amount
$100.00
Your application fee
$10.00
Transferred to seller
$90.00
Refunded to buyer, from your balance
−$100.00
Reversed from seller
$0.00
Your net position
−$90.00
const charge = await stripe.charges.retrieve(chargeId, {
  expand: ['transfer'],
});
const transfer = charge.transfer as Stripe.Transfer;

const expected = Math.round(
  (charge.amount_refunded / charge.amount) * transfer.amount,
);
const actual = transfer.amount_reversed ?? 0;
const missing = expected - actual;

// Tolerate rounding noise; anything above this is real money.
if (missing > 2) flag(chargeId, missing);

Where the partial-refund leak hides

Most teams that know about reverse_transfer set it only on full refunds, because the obvious test case — refund an order completely, watch the money return — passes.

Partial refunds then leak indefinitely. On platforms with shipping adjustments, partial cancellations, or goodwill credits, this is frequently the larger number in aggregate, because each individual instance is too small to notice.

Recovering what has already leaked

A standalone reversal can be created against the transfer at any point while the connected account has balance. The amount is capped at what remains unreversed.

Timing dominates the outcome. Once the seller has paid out, the reversal still succeeds but leaves them negative, and Stripe recovers it only from their future volume. Recovering inside the payout window is the difference between a bookkeeping entry and a write-off.

await stripe.transfers.createReversal(
  transfer.id,
  { amount: missing, refund_application_fee: true },
  { idempotencyKey: `recovery-${chargeId}` },
);

Doing it continuously instead of once

A 90-day sweep tells you what you have already lost. It does not stop the next one, and the recovery window on anything it finds has usually closed.

FeeGuard runs the same comparison on every charge.refunded event as it arrives, which is early enough to act. Detection is free — you pay a share only of what is actually recovered.