DEV Community

Veristria
Veristria

Posted on Originally published at feeguard.dev

What a lost dispute actually costs a Connect platform

Refunds get the attention. Disputes are worse, and the reason is structural rather than a matter of degree.

On a refund you at least have a parameter. reverse_transfer: true exists, you can pass it, and if you forget there is a boolean to point at afterwards. On a dispute there is no such parameter, because there is no such option.

What Stripe does when you lose

Stripe debits the platform for the disputed amount plus the dispute fee. That is it. That is the whole automatic behaviour.

The connected account that received the original transfer is not touched. Not reduced, not notified, not flagged. From that seller's perspective nothing happened at all — the money arrived weeks ago and is still theirs.

So the platform's position after losing a dispute on a destination charge is:

- disputed amount   (debited from platform balance)
- dispute fee       (debited from platform balance, typically ~15 USD)
+ nothing            (the transfer is untouched)
Enter fullscreen mode Exit fullscreen mode

The seller keeps the funds. You paid the customer back and paid a fee for the privilege.

Why this is not symmetrical with refunds

A refund is initiated by you, so Stripe can offer a parameter at the moment of initiation. A dispute is initiated by the cardholder's bank, days or weeks after the charge, through a process you are not in. There is no call of yours to attach a flag to.

The reversal, if you want one, has to be a separate action you take afterwards:

// on charge.dispute.closed with status 'lost'
await stripe.transfers.createReversal(transferId, {
  amount: disputedAmount,       // not necessarily the full transfer
  description: `dispute ${disputeId}`,
});
Enter fullscreen mode Exit fullscreen mode

Nothing calls that for you. If your webhook handler for charge.dispute.closed only writes a row and updates a status, the clawback never happens.

Three things that make this accumulate quietly

The timing hides it. A dispute closes 30 to 90 days after the charge. By then the transfer is in a different payout, a different month, and usually a different reconciliation report. The debit and the un-reversed transfer are never adjacent in any view you look at.

The connected balance may not cover it. Even when you do reverse, the seller may have already paid out. The reversal leaves the account negative, which surfaces later as a failed payout — a third event, in a fourth place, with no obvious link back to a dispute.

Winning still costs. The dispute fee is generally not returned on a win in most regions. A platform with a good win rate still bleeds fees, and those fees are charged to the platform rather than to the seller whose transaction caused them.

The reconciliation query

The question is the same shape as the refund one, and it is worth running over full history rather than a recent window, because the whole failure mode is that it is old by the time it matters:

SELECT d.id                AS dispute_id,
       d.charge_id,
       d.amount            AS debited,
       d.fee               AS dispute_fee,
       COALESCE(r.amount, 0) AS reversed,
       d.amount - COALESCE(r.amount, 0) AS unrecovered
  FROM disputes d
  LEFT JOIN transfer_reversals r ON r.charge_id = d.charge_id
 WHERE d.status = 'lost'
   AND d.amount > COALESCE(r.amount, 0)
 ORDER BY unrecovered DESC;
Enter fullscreen mode Exit fullscreen mode

The policy question underneath the technical one

Whether to claw back is genuinely a business decision, and reasonable platforms answer it differently. A marketplace with sellers who have no control over fraud may absorb disputes deliberately. A platform whose sellers do control fraud usually should not.

What is not a decision is doing it by accident. If you have never reversed a transfer on a lost dispute, you have chosen to absorb every one of them — you just did not know that was the choice you were making. Run the query and find out which platform you are.

Top comments (0)