KulPayAPI Live

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

NameInTypeDescription
account_idquerystring
modequerystring
statusquerystring

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

FieldTypeDescription
amountstring
deadline_daysinteger
notestring
payment_id requiredstring
reason requiredstring Why the request is made, for the approver.
reason_codestring

Responses

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

NameInTypeDescription
id requiredpathstring

Request body application/json required

FieldTypeDescription
outcome requiredstring
One of: won, lost
reason requiredstring

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

NameInTypeDescription
statusquerystring
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

NameInTypeDescription
id requiredpathstring

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

NameInTypeDescription
id requiredpathstring

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

NameInTypeDescription
id requiredpathstring
Idempotency-Key requiredheaderstring

Request body application/json required

FieldTypeDescription
filesobject[]
files[].field requiredstring
files[].file_id requiredstring
textobject

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"
}