Skip to main content

Disputes and chargebacks

Available on rails that report disputes

A dispute reaches you as an event from the rail, so these endpoints apply on a rail that raises dispute events. Chargebacks are a card-scheme mechanism and card rails are the usual source, but the condition is the event rather than the method.

The rails enabled today raise payment events only, so nothing raises a dispute against a payment taken on them and the dispute endpoints answer with an empty list rather than an incomplete one. They switch on with a rail that reports disputes — see your dashboard, or ask us.

A dispute (also called a chargeback) is raised when a customer's bank challenges a payment on the customer's behalf: the customer claims they did not authorize it, did not receive what they paid for, or were charged incorrectly. The disputed amount is held or returned to the customer while the dispute runs, and you get a window to either concede it or contest it with evidence.

This guide is the merchant-facing workflow: the lifecycle, how to respond, how to submit evidence, the timelines, and how disputes surface on your webhooks. The evidence vocabulary and the full evidence field list are on this page; the API reference carries the per-endpoint request and response schemas for the whole dispute surface — listing, filtering and totals, accepting, and the four evidence operations — plus the Files endpoints that evidence uploads go through.

The lifecycle

  1. A dispute arrives with status dispute_opened and a challenge_required_by deadline.
  2. You choose: accept (concede) or submit evidence (contest).
  3. If you accept, it moves to dispute_accepted and the funds are returned to the customer.
  4. If you submit evidence, it moves to dispute_challenged while the bank reviews, and resolves as dispute_won (in your favour) or dispute_lost (against you).
  5. If you do nothing before the deadline, it moves to dispute_expired.

Statuses

StatusMeaning
dispute_openedNew dispute; action needed before challenge_required_by.
dispute_challengedYou submitted evidence; awaiting the outcome.
dispute_acceptedYou conceded; funds returned to the customer.
dispute_expiredThe response window elapsed without a challenge.
dispute_cancelledThe dispute was withdrawn or cancelled.
dispute_wonResolved in your favour.
dispute_lostResolved against you; funds are returned to the customer.

Disputes also carry a stage (pre_dispute, dispute, pre_arbitration, arbitration, dispute_reversal). A pre_dispute (an inquiry or retrieval request) can often be resolved by refunding or providing information before it escalates into a formal dispute. Branch your automation on dispute_status and dispute_stage, never on message text.

How to respond

For each open dispute you make one decision: accept or contest.

Accept (concede)

If the dispute is valid, or not worth contesting, accept it. The disputed funds are returned to the customer and the dispute moves to dispute_accepted. Accepting is final.

POST /disputes/accept/{dispute_id} (no body):

curl -X POST https://api.tensorrail.com/disputes/accept/dp_lbmqfrx2vd94 \
-H "api-key: rail_full_test_xxx"

Contest (submit evidence)

If the charge was legitimate, contest it by submitting evidence before challenge_required_by. On success the dispute moves to dispute_challenged and the bank reviews your evidence.

You may already have refunded the customer separately. Check is_already_refunded on the dispute before contesting: when it is true, a lost dispute would return the funds twice, so accepting (or having already refunded) is usually the right call rather than contesting.

Submitting evidence

Evidence submission is a two-part flow: upload each supporting file, then submit the evidence referencing those files plus any text fields.

Step 1: Upload each file

PUT /disputes/evidence (multipart) uploads one file and returns a file id. Provide the dispute_id and an evidence_type describing what the file is.

curl -X PUT https://api.tensorrail.com/disputes/evidence \
-H "api-key: rail_full_test_xxx" \
-F "dispute_id=dp_lbmqfrx2vd94" \
-F "evidence_type=receipt" \
-F "file=@receipt.pdf"
# → { "file_id": "file_h2xkpqr81m", … }

Repeat for each file (receipt, shipping documentation, customer communication, and so on). The allowed evidence_type values are:

cancellation_policy, customer_communication, customer_signature, receipt, refund_policy, service_documentation, shipping_documentation, invoice_showing_distinct_transactions, recurring_transaction_agreement, uncategorized_file.

Step 2: Submit the evidence

POST /disputes/evidence with the dispute_id, the file ids from step 1, and any text fields (product description, customer email, purchase IP, shipping details, and so on).

curl -X POST https://api.tensorrail.com/disputes/evidence \
-H "api-key: rail_full_test_xxx" \
-H "Content-Type: application/json" \
-d '{
"dispute_id": "dp_lbmqfrx2vd94",
"product_description": "Annual Pro subscription",
"customer_email_address": "buyer@example.com",
"customer_purchase_ip": "203.0.113.7",
"receipt": "file_h2xkpqr81m",
"customer_communication": "file_t7ydwnc402",
"uncategorized_text": "Customer used the service for 3 weeks after purchase."
}'

Submit the strongest evidence for the dispute reason: proof of delivery for "not received", usage logs and terms for "not as described", and a signed agreement for a recurring charge. You can also do all of this from the dashboard without code.

The full set of fields POST /disputes/evidence accepts, alongside dispute_id:

FieldWhat it carries
access_activity_logLogs showing the customer used the service
billing_address, shipping_addressThe customer's addresses
cancellation_policy, refund_policyFile ids of the policy documents
cancellation_policy_disclosure, refund_policy_disclosureHow the policy was shown before purchase
cancellation_rebuttal, refund_refusal_explanationWhy the cancellation or refund does not apply
customer_communicationFile id of correspondence with the customer
customer_email_address, customer_name, customer_purchase_ipWho bought, and from where
customer_signatureFile id of a signature
product_descriptionWhat was sold
receiptFile id of the receipt
service_date, service_documentationWhen the service was delivered, and the file proving it
shipping_carrier, shipping_date, shipping_tracking_number, shipping_documentationDelivery evidence
invoice_showing_distinct_transactionsFile id proving two charges were separate, for a "charged twice" claim
recurring_transaction_agreementFile id of the signed recurring agreement
uncategorized_file, uncategorized_textAnything else that supports your case

Every *_documentation, *_policy, receipt, *_file, customer_communication, customer_signature and invoice_showing_distinct_transactions field takes a file id from step 1; the rest take text.

Timelines

  • Every dispute carries a challenge_required_by deadline. Respond before it. If it passes with no action, the dispute expires as if conceded.
  • After you submit evidence, the outcome (dispute_won / dispute_lost) is decided by the cardholder's bank, not by TensorRail, and can take from days to weeks depending on the stage and scheme.
  • Do not wait until the last day. Gather evidence as soon as dispute_opened arrives, so a missing file does not cost you the deadline.

How disputes appear on webhooks

Every dispute state change is delivered as a dispute_* webhook, so you can automate your response workflow rather than polling:

dispute_opened, dispute_challenged, dispute_won, dispute_lost, dispute_accepted, dispute_expired, dispute_cancelled.

A practical automation:

function onDisputeWebhook(event) {
const { dispute_id, dispute_status, payment_id, challenge_required_by } = event.content.object;
switch (dispute_status) {
case "dispute_opened":
// Create an internal task with the deadline; pull evidence for payment_id.
openDisputeTask(dispute_id, payment_id, challenge_required_by);
break;
case "dispute_won":
closeDisputeTask(dispute_id, "won");
break;
case "dispute_lost":
closeDisputeTask(dispute_id, "lost"); // funds returned to the customer
break;
default:
updateDisputeTask(dispute_id, dispute_status);
}
}

As with all webhooks, verify the signature, respond 2xx quickly, and make the handler idempotent (de-dupe on event_id). See Handle webhooks.

Reducing disputes

  • Clear statement descriptor. Set statement_descriptor_name so the charge is recognisable on the customer's statement.
  • Refund promptly when a customer complains directly, before it becomes a dispute. A pre_dispute inquiry can often be resolved this way.
  • Keep records. Retain receipts, delivery proof, and customer communications, so contesting is a lookup rather than a scramble.

Next steps

You can receive, contest, and resolve a dispute. Round it out:

  • Handle webhooks: reliably receive the dispute_* events that drive your workflow.
  • Refunds: resolve a complaint before it ever becomes a dispute.
  • API reference: Disputes: every dispute and Files endpoint, with request and response schemas.

TensorRail, Limassol, Cyprus.