Orders

Disputes and Chargebacks

A disputed payment is money you booked that is now contested — a card chargeback, or a B2B buyer refusing a Net-N invoice. Throttle surfaces both through the same three fields on the payment, written either by your own call or by the processor.

A dispute is a flag, not a lifecycle object
If you are coming from a processor with a first-class Dispute resource, calibrate expectations: Throttle stores disputed, disputeReason, and disputeOpenedAt on the payment and nothing else. There is no evidence submission, no due date, no dispute id on the payment, and no won/lost state. The flag exists so a contested payment stops looking like clean revenue in your reporting and your AR views — contest the chargeback in your processor's dashboard.

Two ways a payment gets flagged

  1. You flag it. POST /api/v1/payments/{id}/dispute — mainly for Net-N receivables, where the dispute arrives as an email from a buyer rather than a processor event.
  2. The processor flags it. Inbound dispute.* and chargeback.* webhooks are matched to the payment by processor transaction id and write the same three fields. No configuration needed.

Because both paths converge on the same columns, a consumer of payment.disputed does not need to care which one fired.

Flagging a payment

Scope payment_disputes:write. reason is required, 1–500 characters. disputeOpenedAt is stamped server-side.

Flag a disputed invoice
curl -X POST https://api.usethrottle.dev/api/v1/payments/pay_88ce.../dispute \
  -H "X-API-Key: $THROTTLE_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "Buyer claims the invoice was already settled by wire" }'
200 response
{
  "data": {
    "id": "pay_88ce...",
    "disputed": true,
    "disputeReason": "Buyer claims the invoice was already settled by wire",
    "disputeOpenedAt": "2026-08-09T14:41:02.000Z"
  }
}

Flagging an already-flagged payment is a 409 already_disputed rather than a silent overwrite, so you cannot lose the original reason by retrying.

Clearing a dispute

Clear the flag
curl -X POST https://api.usethrottle.dev/api/v1/payments/pay_88ce.../dispute/clear \
  -H "X-API-Key: $THROTTLE_SECRET_KEY"

Clearing sets disputed: false but deliberately leaves disputeReason and disputeOpenedAt in place, so the history of the dispute survives on the payment. It takes no body, and clearing a payment that was never disputed succeeds without emitting an event.

How processor events are classified

Inbound dispute events are classified from the event suffix and the target's status:

Processor signalOutcomeEffect
Status won or reversedClearSets disputed: false, emits payment.dispute_cleared
Status lostLostLeaves disputed: true — see the caveat below
Anything elseOpenSets disputed: true, emits payment.disputed once
A lost chargeback stays flagged
Losing a chargeback leaves disputed: true — the same state as one still in progress. Throttle does not record the outcome, so disputed means "contested at some point and not resolved in your favour", not "currently open". Don't build an open-disputes worklist from this field alone; reconcile against your processor.

A resolution event for a payment that is not currently flagged is a no-op, and redeliveries of an open event will not re-fire payment.disputed — it is emitted only on the first transition into the disputed state.

Webhooks

payment.disputed
{
  "type": "payment.disputed",
  "data": {
    "paymentId": "pay_88ce...",
    "reason": "fraudulent",
    "openedAt": "2026-08-09T14:41:02.000Z"
  }
}

payment.dispute_cleared carries only paymentId. Subscribing to either requires payment_disputes:read. Both are worth routing to a human — a chargeback is one of the few events where nobody finds out until a payout is short. See Webhooks.

Error codes

CodeHTTPMeaning
already_disputed409The payment is already flagged. Clear it first.
invalid_status409The payment is voided or refunded; there is nothing to dispute.
not_found404No payment with that id in this workspace environment.