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.
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
- 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. - The processor flags it. Inbound
dispute.*andchargeback.*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.
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" }' {
"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
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 signal | Outcome | Effect |
|---|---|---|
Status won or reversed | Clear | Sets disputed: false, emits payment.dispute_cleared |
Status lost | Lost | Leaves disputed: true — see the caveat below |
| Anything else | Open | Sets disputed: true, emits payment.disputed once |
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
{
"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
| Code | HTTP | Meaning |
|---|---|---|
already_disputed | 409 | The payment is already flagged. Clear it first. |
invalid_status | 409 | The payment is voided or refunded; there is nothing to dispute. |
not_found | 404 | No payment with that id in this workspace environment. |