API reference
Disputes
Chargebacks and claims against payments: held, answered with evidence, won or lost (F2-5).
GET /v1/admin/disputes
List every dispute
Every account's disputes, waiting first. Requires admin:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
account_id | query | string | |
mode | query | string | |
status | query | string |
Responses
- 200
{data, total}.
Request
curl -X GET "$KULPAY_URL/v1/admin/disputes" \ -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"data": [
{
"account_id": "3KZezGk2wQ4eVb9nC6xHs1TrPuD",
"amount": "400",
"created_at": "2026-10-12T09:14:03Z",
"currency": "MZN",
"deadline": "2026-10-22T23:59:59Z",
"dispute_ref": "dp_1Q8xYz",
"fee": "0",
"id": "3KZf0Qd9XyFhR2m8AqgP1sLwZbN",
"mode": "live",
"payment_id": "3KZezI1uT8vXcW0qJdYp7nRkLmA",
"provider_reason": "product_not_received",
"reason": "product_not_received",
"source": "provider",
"status": "needs_response",
"updated_at": "2026-10-12T09:14:03Z"
}
],
"total": 1
}
POST /v1/admin/disputes
Open a dispute (maker)
For a rail without acquirer disputes (a complaint on M-Pesa or e-Mola): prepares a dispute.open approval a different person decides (POST /v1/admin/approvals/{id}/decide). Approved, the dispute opens (source: operator), the amount is held and the payment becomes disputed. Only a succeeded payment can be disputed; amount defaults to the payment's, deadline_days to 7. Requires admin:write.
Request body application/json required
| Field | Type | Description |
|---|---|---|
amount | string | |
deadline_days | integer | |
note | string | |
payment_id required | string | |
reason required | string | Why the request is made, for the approver. |
reason_code | string |
Responses
- 202 Prepared; a second person must approve it.
- 400
invalid_request.
Request
curl -X POST "$KULPAY_URL/v1/admin/disputes" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"deadline_days": 10,
"note": "the payer says the phone was stolen",
"payment_id": "3KZezI1uT8vXcW0qJdYp7nRkLmA",
"reason": "complaint 4471",
"reason_code": "fraudulent"
}'
202 Response application/json
{
"approval": {
"created_at": "2026-10-12T09:20:00Z",
"id": "3KZf2xY4zA6bC8dE0fG2hI4jK6l",
"kind": "dispute.open",
"prepared_by": "key_ops_ana",
"reason": "complaint 4471",
"status": "pending",
"subject_id": "3KZezI1uT8vXcW0qJdYp7nRkLmA",
"tenant_id": "3KZezGk2wQ4eVb9nC6xHs1TrPuD"
},
"message": "dispute prepared; a second person must approve it (POST /v1/admin/approvals/{id}/decide)"
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "only a succeeded payment can be disputed (this one is failed)",
"error": {
"code": "invalid_request",
"message": "only a succeeded payment can be disputed (this one is failed)"
},
"status": 400,
"title": "Bad Request",
"type": "about:blank"
}
POST /v1/admin/disputes/{id}/decide
Rule on a dispute (maker)
An operator's ruling, won or lost, for a dispute no acquirer decides: prepares a dispute.decide approval for a second person. Approved, won puts the amount back and the payment is succeeded again; lost keeps it and charges the chargeback fee. Requires admin:write.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string |
Request body application/json required
| Field | Type | Description |
|---|---|---|
outcome required | string | One of: won, lost |
reason required | string |
Responses
- 202 Prepared; a second person must approve it.
- 404 No such dispute (or another account's).
not_found - 409
invalid_state: no longer waiting for a response.
Request
curl -X POST "$KULPAY_URL/v1/admin/disputes/{id}/decide" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"outcome": "won",
"reason": "the payer recognised the payment"
}'
202 Response application/json
{
"approval": {
"created_at": "2026-10-12T09:20:00Z",
"id": "3KZf2xY4zA6bC8dE0fG2hI4jK6l",
"kind": "dispute.decide",
"prepared_by": "key_ops_ana",
"reason": "complaint 4471",
"status": "pending",
"subject_id": "3KZezI1uT8vXcW0qJdYp7nRkLmA",
"tenant_id": "3KZezGk2wQ4eVb9nC6xHs1TrPuD"
},
"message": "ruling prepared; a second person must approve it (POST /v1/admin/approvals/{id}/decide)"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such dispute",
"error": {
"code": "not_found",
"message": "no such dispute"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
409 Error · invalid_state application/problem+json
{
"code": "invalid_state",
"detail": "The dispute is no longer waiting for a response.",
"error": {
"code": "invalid_state",
"message": "The dispute is no longer waiting for a response."
},
"status": 409,
"title": "Invalid state",
"type": "about:blank"
}
GET /v1/disputes
List disputes
The account's disputes, those waiting first, the soonest deadline first. A dispute is opened by the acquirer (Stripe charge.dispute.created: source: provider) or by an operator on a rail without acquirer disputes (source: operator); its amount moves from the available balance to disputes_hold. Answer with evidence before deadline: it is under_review until decided. won puts the amount back; lost keeps it and charges the chargeback fee of the commission schedule (a rule of type DISPUTE). Reminders (dispute.deadline_approaching) go out 5 days and 1 day before the deadline; a dispute unanswered by then is lost. Webhooks: dispute.created, dispute.evidence_submitted, dispute.deadline_approaching, dispute.won, dispute.lost. Requires payments:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
status | query | string | One of: needs_response, under_review, won, lost |
Responses
- 200
{data, total}.
Request
curl -X GET "$KULPAY_URL/v1/disputes" \ -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"data": [
{
"account_id": "3KZezGk2wQ4eVb9nC6xHs1TrPuD",
"amount": "400",
"created_at": "2026-10-12T09:14:03Z",
"currency": "MZN",
"deadline": "2026-10-22T23:59:59Z",
"dispute_ref": "dp_1Q8xYz",
"fee": "0",
"id": "3KZf0Qd9XyFhR2m8AqgP1sLwZbN",
"mode": "live",
"payment_id": "3KZezI1uT8vXcW0qJdYp7nRkLmA",
"provider_reason": "product_not_received",
"reason": "product_not_received",
"source": "provider",
"status": "needs_response",
"updated_at": "2026-10-12T09:14:03Z"
}
],
"total": 1
}
GET /v1/disputes/{id}
Get a dispute
One of the account's disputes. Requires payments:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string |
Responses
- 200 The dispute.
- 404 No such dispute (or another account's).
not_found
Request
curl -X GET "$KULPAY_URL/v1/disputes/{id}" \
-H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"account_id": "3KZezGk2wQ4eVb9nC6xHs1TrPuD",
"amount": "400",
"created_at": "2026-10-12T09:14:03Z",
"currency": "MZN",
"deadline": "2026-10-22T23:59:59Z",
"dispute_ref": "dp_1Q8xYz",
"fee": "0",
"id": "3KZf0Qd9XyFhR2m8AqgP1sLwZbN",
"mode": "live",
"payment_id": "3KZezI1uT8vXcW0qJdYp7nRkLmA",
"provider_reason": "product_not_received",
"reason": "product_not_received",
"source": "provider",
"status": "needs_response",
"updated_at": "2026-10-12T09:14:03Z"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such dispute",
"error": {
"code": "not_found",
"message": "no such dispute"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
POST /v1/disputes/{id}/accept
Concede a dispute
The merchant does not contest: the dispute is lost now (dispute.lost, close_reason "accepted by the merchant"), with the chargeback fee. Requires payments:write.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string |
Responses
- 200 The dispute, lost.
- 404 No such dispute (or another account's).
not_found - 409
invalid_state: no longer waiting for a response.
Request
curl -X POST "$KULPAY_URL/v1/disputes/{id}/accept" \
-H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"account_id": "3KZezGk2wQ4eVb9nC6xHs1TrPuD",
"amount": "400",
"close_reason": "accepted by the merchant",
"closed_at": "2026-10-13T08:00:00Z",
"created_at": "2026-10-12T09:14:03Z",
"currency": "MZN",
"deadline": "2026-10-22T23:59:59Z",
"dispute_ref": "dp_1Q8xYz",
"fee": "150",
"id": "3KZf0Qd9XyFhR2m8AqgP1sLwZbN",
"mode": "live",
"payment_id": "3KZezI1uT8vXcW0qJdYp7nRkLmA",
"provider_reason": "product_not_received",
"reason": "product_not_received",
"source": "provider",
"status": "lost",
"updated_at": "2026-10-12T09:14:03Z"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such dispute",
"error": {
"code": "not_found",
"message": "no such dispute"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
409 Error · invalid_state application/problem+json
{
"code": "invalid_state",
"detail": "The dispute is no longer waiting for a response.",
"error": {
"code": "invalid_state",
"message": "The dispute is no longer waiting for a response."
},
"status": 409,
"title": "Invalid state",
"type": "about:blank"
}
POST /v1/disputes/{id}/evidence
Answer a dispute with evidence
Submits the evidence to the acquirer and moves the dispute to under_review (dispute.evidence_submitted). text takes the card networks' evidence fields (product_description, customer_name, customer_email_address, billing_address, service_date, shipping_date, shipping_carrier, shipping_tracking_number, refund_policy_disclosure, refund_refusal_explanation, cancellation_rebuttal, duplicate_charge_explanation, access_activity_log, uncategorized_text); files attaches documents uploaded first to the Files API with purpose dispute_evidence, each as one of receipt, customer_communication, customer_signature, refund_policy, cancellation_policy, service_documentation, shipping_documentation, duplicate_charge_documentation, uncategorized_file. Only once: a retry with the same Idempotency-Key returns the dispute. Requires payments:write.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string | |
Idempotency-Key required | header | string |
Request body application/json required
| Field | Type | Description |
|---|---|---|
files | object[] | |
files[].field required | string | |
files[].file_id required | string | |
text | object |
Responses
- 200 The dispute, under review.
- 400
invalid_request: no evidence, an unknown field, or a file that is not this account's dispute evidence. - 404 No such dispute (or another account's).
not_found - 409
invalid_state: no longer waiting for a response.
Request
curl -X POST "$KULPAY_URL/v1/disputes/{id}/evidence" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"files": [
{
"field": "customer_communication",
"file_id": "3KZf1aB7cD9eF0gH2iJ4kL6mN8p"
}
],
"text": {
"product_description": "Two bags of cement, delivered on 3 October to the address on the order"
}
}'
200 Response application/json
{
"account_id": "3KZezGk2wQ4eVb9nC6xHs1TrPuD",
"amount": "400",
"created_at": "2026-10-12T09:14:03Z",
"currency": "MZN",
"deadline": "2026-10-22T23:59:59Z",
"dispute_ref": "dp_1Q8xYz",
"evidence_submitted_at": "2026-10-14T10:02:44Z",
"fee": "0",
"id": "3KZf0Qd9XyFhR2m8AqgP1sLwZbN",
"mode": "live",
"payment_id": "3KZezI1uT8vXcW0qJdYp7nRkLmA",
"provider_reason": "product_not_received",
"reason": "product_not_received",
"source": "provider",
"status": "under_review",
"updated_at": "2026-10-12T09:14:03Z"
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "unknown evidence field \"favourite_colour\"",
"error": {
"code": "invalid_request",
"message": "unknown evidence field \"favourite_colour\""
},
"status": 400,
"title": "Bad Request",
"type": "about:blank"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such dispute",
"error": {
"code": "not_found",
"message": "no such dispute"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
409 Error · invalid_state application/problem+json
{
"code": "invalid_state",
"detail": "The dispute is no longer waiting for a response.",
"error": {
"code": "invalid_state",
"message": "The dispute is no longer waiting for a response."
},
"status": 409,
"title": "Invalid state",
"type": "about:blank"
}
KulPay