Skip to main content

Disputes

Contract §8. Section numbers are contract-wide and never change; the contract overview maps every section to its page.

Conventions — authentication, the response envelope, idempotency, retry safety, pagination, rate limits — are defined in the Partner Integration Guide and apply to every endpoint here.

Contesting a charge with the card network. Nine endpoints. Filing one is a compliance action with a deadline attached — read What you do before wiring it.

The dispute object

{
"id": "dsp_01M0VTTGGG771N36KJS697AKF1", // OUR id. The only id these endpoints accept.
"clientId": "cl_01M0C17MNJJKY8N7D5YEZ22C6W",
"transactionId": "e2295872-f4c4-4882-a424-2eca5c3fb929", // the ISSUER's id — see §4
"status": "PENDING",
"type": "FRAUD",
"textEvidence": "The card was in my possession and I did not make this purchase.",
"amount": { "value": "25.00", "currency": "USD" },
"createdAt": "2026-08-25T08:45:04.574Z",
"updatedAt": "2026-08-25T08:46:05.978Z",
"resolvedAt": "2026-08-25T08:48:21.212Z", // only once terminal
"transaction": { // absent on a freshly created dispute
"id": "e2295872-f4c4-4882-a424-2eca5c3fb929",
"amount": { "value": "42.00", "currency": "USD" }, // the FULL charge
"merchantName": "TEST MERCHANT",
"postedAt": "2026-08-25T08:44:52.113Z",
"cardholderId": "chd_01M0C17MNJJKY8N7D5YEZ22C6W" // single-dispute read only
},
"attachmentCount": 2,
"hasFileEvidence": true // single-dispute read only
}

Money is a decimal string with an ISO 4217 alpha-3 code, exactly as in §4 — never minor units. amount on the dispute is what you are contesting; transaction.amount is the whole charge. They are equal unless you filed for less.

Fields are ABSENT, not null, when we do not have them. hasFileEvidence and transaction.cardholderId come back on GET /disputes/{id} only — the issuer does not report them on a list row, and a freshly created dispute has no transaction block at all. Treat a missing key as "not stated here", never as false.

Dispute status, and what you do

statusMeansWhat you do
PENDINGFiled, awaiting review.Add evidence while you can. Watch for a request for more.
IN_REVIEWWith the issuer or the network.Act if evidence is requested — there is a deadline.
ACCEPTEDWon.Nothing. The account is credited and a chargeback transaction appears in §4.
REJECTEDLost. The charge stands.Nothing on this dispute.
CANCELEDWithdrawn — by you, or invalidated.Nothing. You may file a new dispute on the transaction.
RESOLVED_BY_MERCHANTThe merchant refunded directly.Nothing. The money is back, and this is NOT a chargeback — the credit is the merchant's refund settlement.
OTHERThe issuer reported a state this API does not model yet.Read the dispute again later, and tell us. Published rather than hidden so a dispute never vanishes from your list for being unfamiliar.

The last four are terminal: resolvedAt is set and nothing further can be filed. This API answers 422 DISPUTE_REJECTED on a write to one. (The issuer itself accepts such writes and files them nowhere — we refuse rather than report a success that did not happen.)

One exception: cancelling an already-CANCELED dispute is a 200 no-op, returning the dispute as it stands. Cancel takes no Idempotency-Key, so a retry landing twice is ordinary and must not read as a caller error — the same rule as POST /cards/:id/cancel. Cancelling one the issuer resolved (ACCEPTED, REJECTED, RESOLVED_BY_MERCHANT) still answers 422: those are conclusions, and reporting "canceled" would misstate who ended the dispute.

The dispute type

FRAUD · CREDIT_NOT_PROCESSED · SERVICE_NOT_RECEIVED · MERCHANDISE_ISSUE · OTHER

Required when filing, and fixed once filed. Sending type on a PATCH answers 422 DISPUTE_REJECTED rather than silently doing nothing; to change it, cancel and file again.

Dispute thresholds and fees

This is the issuer's rule and neither the service nor you can change it:

  • $10 or under: not filed with the network and not reimbursed.
  • From 2026-09-01, under $30: not filed and not charged.
  • From 2026-09-01, each dispute of $30 or more that is escalated carries a fee, whatever the outcome — including one you lose.

A dispute below the threshold is still accepted here and still gets a dsp_ id. It simply goes no further. Surface that to your users rather than letting a $4 dispute sit in PENDING.

Dispute endpoints

MethodPathNotes
POST/transactions/{transactionId}/disputes201. The transaction must have POSTED.
GET/disputes?client_id=&cardholder_id=&transaction_id=&status=&cursor=&_limit=Keyset. client_id is required; the rest narrow.
GET/disputes/{id}The only shape with hasFileEvidence and the cardholder.
PATCH/disputes/{id}{ "textEvidence": "…" }. Replaces in full — not an append.
POST/disputes/{id}/cancel200, bodyless, terminal. A repeat is a no-op, not a conflict.
GET/disputes/{id}/attachmentsMetadata only.
POST/disputes/{id}/attachmentsmultipart/form-data, one attachment part. 201.
GET/disputes/{id}/attachments/{attachmentId}The file itself — see below.
DELETE/disputes/{id}/attachments/{attachmentId}204.

One open dispute per transaction. A second answers 409 DISPUTE_ALREADY_OPEN. Once the first is terminal you may file again — that is the recovery path from a mistyped dispute: cancel, then re-file with the right type.

client_id is required on the list and there is no cross-client read. A dispute carries no owner information of its own, so the only way to scope a list correctly is per client.

cardholder_id and transaction_id narrow within that client — the same two narrowings §4 offers on transactions. A cardholder_id belonging to one of your OTHER clients answers 404 CARDHOLDER_NOT_FOUND rather than quietly widening the query.

No _page. Keyset only: follow nextCursor until it is null.

Attachments

Additive, maximum 20 per dispute; the 21st answers 409 ATTACHMENT_LIMIT_REACHED. Max 20 MB each. The service stores nothing — the bytes are relayed and never written down.

attachmentId is the issuer's id, the one place besides the transaction id where that is true. An attachment is only reachable through a dispute you have already proved you own, and it appears in no webhook, so it needs no id of ours.

Downloads always come back as application/octet-stream with Content-Disposition: attachment, whatever the file was uploaded as. Do not rely on the response Content-Type to tell you the format — use mimetype from the attachment list.

There is no "replace all evidence" endpoint, deliberately. The issuer has one and we do not relay it: it deletes every attachment already on the dispute and replaces them with the single file uploaded. We measured it — three files in, one left, no warning. POST /disputes/{id}/attachments does the same job without destroying your case. If you have read the issuer's own documentation and are looking for that endpoint, this is why it is missing.

Dispute error codes

DISPUTE_NOT_FOUND (404 — no such dispute, or one belonging to another partner), CARDHOLDER_NOT_FOUND (404 — the cardholder_id filter names someone who is not under the client in your client_id), TRANSACTION_NOT_SETTLED (422 — the charge has not posted yet; wait, do not change the request), DISPUTE_ALREADY_OPEN (409 — that transaction already has an open dispute; read it), DISPUTE_REJECTED (422 — refused: an unchangeable field, a resolved dispute, or the issuer declining), ATTACHMENT_NOT_FOUND (404), ATTACHMENT_LIMIT_REACHED (409 — 20 already), ATTACHMENT_TOO_LARGE (413 — fix the FILE, not the request), INVALID_ATTACHMENT (422 — empty or unusable).

POST /transactions/{id}/disputes takes no Idempotency-Key and needs none: the issuer refuses a second dispute on a transaction that has one open. On a 502 PROVIDER_AMBIGUOUS, read GET /disputes with your client_id before retrying — a retry is safe, but checking is faster.