Disputes
A dispute — a chargeback — is a customer formally contesting a payment through their bank. The disputed amount leaves your balance the moment it opens, and a deadline starts ticking. This page covers the full lifecycle from chargeback to arbitration, and the API for fighting or accepting one.
The dispute lifecycle
Two fields track a dispute. status is the working state — who needs to act, and how it resolved. stage is how far the dispute has escalated through the card network's process. The status cycle repeats at each stage:
status | Meaning | Terminal |
|---|---|---|
needs_response | Waiting on you. Submit evidence or accept before evidence_due_by. | No |
under_review | Evidence submitted; the issuer or network is reviewing. Nothing for you to do. | No |
won | Resolved in your favor — the withheld funds return to your balance. | Yes |
lost | Resolved for the cardholder — the withheld funds are gone. | Yes |
accepted | You accepted liability — same money outcome as lost, by your choice. | Yes |
stage | Meaning |
|---|---|
chargeback | The initial dispute. Most disputes start and end here. |
pre_arbitration | The side that lost the chargeback round contested the outcome. The status cycle runs again — a new needs_response, a new deadline, one more chance to submit evidence. |
arbitration | The card network itself decides. Its ruling is binding — there is no further escalation. Arbitration carries network fees for the losing side; going there is worth it only for large amounts with strong evidence. |
Escalation to pre_arbitration arrives as a dispute.updated event: stage moves forward and status resets to needs_response with a fresh evidence_due_by. Nothing about your response is carried over automatically — the second round needs its own evidence submission.
The dispute object
Disputes are created by the network, never by you — the first you hear of one is a dispute.created webhook. Every dispute references the payment it contests:
{
"id": "dp_5Kw2mR8vQz1N",
"object": "dispute",
"payment": "pay_3Vt7Kd9sLm2x",
"amount": 12900,
"currency": "EUR",
"reason": "fraudulent",
"stage": "chargeback",
"status": "needs_response",
"evidence_due_by": "2026-07-30T23:59:59Z",
"livemode": false,
"created_at": "2026-07-16T18:40:22Z"
}
amount is the disputed amount in minor units — it can be less than the payment's amount when the customer contests part of an order. reason is normalized across card networks into five values:
reason | The customer says… | Evidence that wins |
|---|---|---|
fraudulent | "I didn't authorize this." | Proof the cardholder made the order: AVS/CVV passes, 3DS authentication, delivery to the billing address, prior undisputed orders from the same customer. |
product_not_received | "It never arrived." | tracking_number with delivery confirmation, or service_date with proof of provision for services. |
duplicate | "I was charged twice." | Receipts showing two distinct orders — or, if it really is a duplicate, refund one charge and accept the dispute. |
credit_not_processed | "The refund never came." | The refund id (rf_…) and its date if it was issued; your refund_policy and the customer's agreement to it if it was refused. |
general | Anything else. | Everything relevant — order details, communication, delivery proof. |
Money during a dispute
When a dispute opens, the disputed amount is withheld from your balance — it shows on your settlement lines as a dispute debit against the original payment, whichever provider processed it. Then:
- Won: the withheld amount returns to your balance in full.
- Lost or accepted: the funds go to the cardholder and the debit stands.
The dispute keeps the payment's currency; on multicurrency accounts the debit hits the corresponding currency balance. Refunding a disputed payment doesn't resolve the dispute — once a chargeback is open, the money moves through the dispute, not through POST /v1/refunds.
Deadlines — evidence_due_by
Every dispute in needs_response carries evidence_due_by — an ISO 8601 timestamp set by the card network's rules for that stage. It is the single most important field on the object.
evidence_due_by passes with no evidence and no acceptance, the dispute is forfeited — it moves to lost and the funds go to the cardholder. Treat dispute.created as an operational alert with a timer on it, not a notification to read later.
Submit well before the deadline, not at it: evidence is forwarded to the network in the format each scheme expects, and a package submitted with hours to spare leaves no room for a formatting round-trip. Once the dispute resolves, evidence_due_by is null.
Accept or contest — deciding
Two moves exist, and doing nothing is the worst version of accepting. The economics:
| Accept | Contest | |
|---|---|---|
| When | The dispute is legitimate — the fraud was real, the goods didn't ship, the refund slipped. | You have documentary evidence against the stated reason. |
| Money | Withheld funds go to the cardholder now. | Funds return if you win; nothing further is lost if you don't. |
| Time | Immediate, terminal. | Weeks under review; longer if it escalates. |
| Signal | Counts toward your dispute rate either way — accepting doesn't make the dispute not have happened. | Same. |
Fighting a legitimate dispute and losing costs the same as accepting — it just takes longer and adds review work on both sides. Accept the real ones fast, fight the wrong ones hard:
curl -X POST https://api.finscale.dev/v1/disputes/dp_5Kw2mR8vQz1N/accept \
-H "Authorization: Bearer sk_test_51FinscaleDemo…" \
-H "Idempotency-Key: idem_dp_5Kw2mR8vQz1N_accept"
# → { "id": "dp_5Kw2mR8vQz1N", "status": "accepted", "evidence_due_by": null, … }
Accepting is terminal and cannot be undone. Only disputes in needs_response can be accepted — anything else returns dispute_not_actionable (see errors).
Submitting evidence
To contest, send the evidence package to POST /v1/disputes/{id}/evidence. fields carries structured text keyed by field name; files carries supporting documents as HTTPS URLs reachable by Finscale. Finscale forwards the package to the network in the format each scheme expects:
curl https://api.finscale.dev/v1/disputes/dp_5Kw2mR8vQz1N/evidence \
-H "Authorization: Bearer sk_test_51FinscaleDemo…" \
-H "Idempotency-Key: idem_dp_5Kw2mR8vQz1N_evidence" \
-H "Content-Type: application/json" \
-d '{
"fields": {
"product_description": "Wireless headphones, order ord_9f21_0716",
"tracking_number": "3SFINS0912837465",
"shipping_address": "Keizersgracht 12, 1015 CN Amsterdam, NL",
"customer_communication": "Order confirmation and delivery notification, exported as PDF"
},
"files": [
"https://files.example.com/evidence/ord_9f21_0716-delivery.pdf"
]
}'
{
"id": "dp_5Kw2mR8vQz1N",
"object": "dispute",
"payment": "pay_3Vt7Kd9sLm2x",
"amount": 12900,
"currency": "EUR",
"reason": "fraudulent",
"stage": "chargeback",
"status": "under_review",
"evidence_due_by": null,
"livemode": false,
"created_at": "2026-07-16T18:40:22Z"
}
Any fields keys are accepted; these are the ones the networks recognize:
| Field | Use for |
|---|---|
product_description | What was bought, tied to your order reference. |
customer_communication | Confirmations, support threads, anything showing the customer knew about and engaged with the order. |
tracking_number | Carrier tracking for physical goods — the single strongest field for product_not_received. |
shipping_address | Where it went — matching the billing address strengthens fraudulent defenses. |
refund_policy | Your policy text, for credit_not_processed disputes where the refusal was per policy. |
service_date | When a service was provided, for non-shipped goods. |
under_review and no longer actionable until (and unless) it returns to needs_response at a later stage.
Listing and monitoring
Webhooks are the push channel; the list endpoint is the reconciliation view. The filter you'll run daily is the action queue — everything waiting on you:
curl "https://api.finscale.dev/v1/disputes?status=needs_response&limit=20" \
-H "Authorization: Bearer sk_test_51FinscaleDemo…"
{
"object": "list",
"data": [
{
"id": "dp_5Kw2mR8vQz1N",
"object": "dispute",
"payment": "pay_3Vt7Kd9sLm2x",
"amount": 12900,
"currency": "EUR",
"reason": "fraudulent",
"stage": "chargeback",
"status": "needs_response",
"evidence_due_by": "2026-07-30T23:59:59Z",
"livemode": false,
"created_at": "2026-07-16T18:40:22Z"
}
],
"has_more": false,
"url": "/v1/disputes"
}
Filter by status or by payment id; results are most recent first and paginate with starting_after. Retrieve a single dispute with GET /v1/disputes/{id}.
Webhook events
| Event | When |
|---|---|
dispute.created | A dispute opened. Funds are already withheld; the response clock is running. Start here. |
dispute.updated | Stage or status changed without resolving — evidence acknowledged (under_review), or an escalation reset the dispute to needs_response at the next stage. |
dispute.closed | Terminal outcome: won, lost, or accepted. Reconcile the money on this event. |
{
"id": "evt_8Wn3xK7qP2Rd",
"object": "event",
"type": "dispute.created",
"created_at": "2026-07-16T18:40:23Z",
"livemode": false,
"request_id": null,
"data": {
"object": {
"id": "dp_5Kw2mR8vQz1N",
"object": "dispute",
"payment": "pay_3Vt7Kd9sLm2x",
"amount": 12900,
"currency": "EUR",
"reason": "fraudulent",
"stage": "chargeback",
"status": "needs_response",
"evidence_due_by": "2026-07-30T23:59:59Z",
"livemode": false,
"created_at": "2026-07-16T18:40:22Z"
}
}
}
Verify Finscale-Signature before trusting any payload, and treat request_id: null as normal — disputes originate from the network, not from one of your API requests. Delivery ordering is not guaranteed; key your handler on the dispute's current status, not on event arrival order. Details on the Webhooks page.
Error scenarios
| Scenario | Response |
|---|---|
Evidence or accept on a dispute not in needs_response — already under review, already resolved, or a second evidence submission at the same stage |
400 invalid_request_error, code dispute_not_actionable |
| Unknown dispute id | 404 invalid_request_error |
| Deadline passed with no response | Not an API error — the dispute silently becomes lost and dispute.closed fires. |
{
"error": {
"type": "invalid_request_error",
"code": "dispute_not_actionable",
"message": "This dispute is under_review and cannot accept evidence.",
"doc_url": "https://docs.finscale.dev/errors/#dispute_not_actionable",
"request_id": "req_7Hf3kQd2"
}
}
Preventing disputes
The cheapest dispute is the one that never opens:
- Screen the fraud out first. The risk engine blocks or reviews high-risk card payments before authorization — your dispute rate is the lagging measure of how well its thresholds are tuned.
- Authenticate with 3DS. A payment authenticated via 3-D Secure shifts fraud liability to the issuer —
fraudulentdisputes on authenticated payments are largely defensible by design. - Refund fast. A refund issued when the customer first complains costs the disputed amount; the same refund after a chargeback costs the amount plus the dispute.
credit_not_processeddisputes are almost always a race you could have won. - Be recognizable. A clear billing descriptor and an order reference the customer can match to their purchase prevents the "I don't recognize this charge" reflex that becomes a
fraudulentdispute.