Implementation guides
Handling disputes
A dispute (also called a chargeback) happens when a cardholder questions a payment with their bank. When a dispute is opened, the disputed amount is typically withheld until it's resolved. This guide covers how to find disputes and respond to them with evidence.
Before you begin
You don't create disputes — Qualy opens them automatically when a payment provider notifies us of a chargeback, and links each one to the original transaction, payment intent, and contact. Your job is to detect open disputes and respond in time.
Disputes are time-sensitive
A dispute in needs-response (or warning-needs-response) has a deadline. Submit your evidence before it passes, or the dispute is decided against you.
Dispute lifecycle
A dispute's status tells you where it stands and whether it still needs you:
| Status | Meaning |
|---|---|
warning-needs-response | An early warning was raised. You can respond, but no funds have been withdrawn yet. |
warning-under-review | Your response to a warning is being reviewed. |
warning-closed | The warning was resolved and closed. Closed. |
needs-response | A formal dispute is open and requires your evidence before the deadline. |
under-review | Your evidence has been submitted and the bank is reviewing it. |
won | Resolved in your favor — funds are retained. Closed. |
lost | Resolved against you — funds are returned to the cardholder. Closed. |
won, lost, and warning-closed are the closed states; everything else is an open dispute. You can only refund a transaction once any dispute on it is closed.
Dispute reasons
The reason field explains why the cardholder disputed the payment. Possible values: fraudulent, unrecognized, duplicate, credit-not-processed, debit-not-authorized, customer-initiated, bank-cannot-process, incorrect-account-details, insufficient-funds, general, and other. Tailor your evidence to the reason.
List disputes
Fetch disputes with the standard query parameters. Filter by status to find the ones that need action. The array is returned under data.disputes:
curl "https://api.qualyhq.com/v1/disputes?status[0]=needs-response&status[1]=warning-needs-response" \
-H 'Authorization: ApiKey your-api-key-here' \
-H 'X-TENANT-ID: your-tenant-id-here'
const params = new URLSearchParams();
params.append('status[0]', 'needs-response');
params.append('status[1]', 'warning-needs-response');
const res = await fetch(`https://api.qualyhq.com/v1/disputes?${params}`, {
headers: {
'Authorization': 'ApiKey your-api-key-here',
'X-TENANT-ID': 'your-tenant-id-here',
},
});
const { data } = await res.json();
console.log(data.disputes); // array of open disputes
use Illuminate\Support\Facades\Http;
$disputes = Http::withHeaders([
'Authorization' => 'ApiKey your-api-key-here',
'X-TENANT-ID' => 'your-tenant-id-here',
])->get('https://api.qualyhq.com/v1/disputes', [
'status' => ['needs-response', 'warning-needs-response'],
])->json('data.disputes');
Retrieve a single dispute with GET /v1/disputes/{disputeId}. The dispute object references the transaction, paymentIntent, and contact it relates to, plus its amount, currency, status, and reason.
Respond with evidence
Responding takes two steps: upload each evidence file, then submit the dispute with those files attached.
1. Upload your evidence files
Upload each supporting document (receipts, delivery proof, customer communication) through the Storage API and keep the resulting file ID.
2. Submit the dispute
Send a POST to /v1/disputes/{disputeId}/update with submit: true and an evidence array. Each evidence item references an uploaded document (its storage file ID), a type, and optional comments:
curl -X POST https://api.qualyhq.com/v1/disputes/653fc551d14abfe63d4fd48b/update \
-H 'Authorization: ApiKey your-api-key-here' \
-H 'X-TENANT-ID: your-tenant-id-here' \
-H 'Content-Type: application/json' \
-d '{
"submit": true,
"evidence": [
{
"document": "507f1f77bcf86cd799439011",
"type": "service-documentation",
"comments": "Signed enrollment agreement and course access logs."
}
]
}'
const res = await fetch(
'https://api.qualyhq.com/v1/disputes/653fc551d14abfe63d4fd48b/update',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'ApiKey your-api-key-here',
'X-TENANT-ID': 'your-tenant-id-here',
},
body: JSON.stringify({
submit: true,
evidence: [
{
document: '507f1f77bcf86cd799439011',
type: 'service-documentation',
comments: 'Signed enrollment agreement and course access logs.',
},
],
}),
},
);
const { data: dispute } = await res.json();
console.log(dispute.status); // "under-review"
use Illuminate\Support\Facades\Http;
$dispute = Http::withHeaders([
'Authorization' => 'ApiKey your-api-key-here',
'X-TENANT-ID' => 'your-tenant-id-here',
])->post('https://api.qualyhq.com/v1/disputes/653fc551d14abfe63d4fd48b/update', [
'submit' => true,
'evidence' => [
[
'document' => '507f1f77bcf86cd799439011',
'type' => 'service-documentation',
'comments' => 'Signed enrollment agreement and course access logs.',
],
],
])->json('data');
echo $dispute['status']; // "under-review"
The type of each evidence item must be one of service-documentation, customer-communication, activity-log, or uncategorized-file. After a successful submit, the dispute moves to under-review.
Saving a draft vs. submitting
Set submit: false to attach evidence without sending it to the bank yet — useful for assembling your response over time. Once you set submit: true, the evidence is final and the dispute is handed off for review.
Conceding a dispute
If you don't intend to contest the charge, concede it by sending status: "lost". This closes the dispute and returns the funds to the cardholder — do this instead of ignoring a dispute you can't win, to avoid unnecessary fees.
Refunds and disputes
You cannot refund a transaction while a dispute on it is open. Refunds are only allowed once the dispute reaches a closed state (won, lost, or warning-closed). See Issuing refunds.
Next steps
- Reconcile disputes against the original payment with Querying data.
- Learn how funds move in payment splits.
- Handle errors from the dispute endpoints.