Refunds
Return a captured payment to the payer. The recipient and amount are derived from the original payment.
The Refund object
Initiation returns the refund transaction that was created, along with the state of the original payment.
{
"id": "b7c1e2f3-4a5d-6e7f-8901-2b3c4d5e6f70",
"status": "PENDING",
"amount": 5000,
"currency": "XAF",
"originalPaymentId": "a1b2c3d4-5e6f-7890-1a2b-3c4d5e6f7081",
"originalPaymentStatus": "COMPLETED",
"message": "Remboursement initié — transfert en cours vers le payeur"
}Properties
idstringKPay identifier for the refund. Keep it to reconcile with webhooks.
statusstringState at initiation. PENDING: accepted, transfer under way. FAILED: initiation refused, no funds moved.
PENDINGFAILEDamountnumberRefunded amount, equal to the gross amount paid by the customer (fees included).
currencystringRefund currency, identical to the original payment.
originalPaymentIdstringIdentifier of the refunded payment.
originalPaymentStatusstringState of the original payment at initiation. It becomes REFUNDED once the refund is confirmed.
messagestringHuman-readable message describing the outcome of the initiation.
Refund rules
- Full refunds only: there is no amount field. The gross amount paid by the customer is returned, fees included. The refund itself incurs no fee.
- A 7-day window after the payment completes. Beyond that, the request is refused.
- One active refund per payment. A previously failed attempt does not prevent a new one.
- The payment must be an incoming payment with status COMPLETED.
- The amount is reserved on your wallet as soon as the refund is initiated: your available balance drops immediately. If the balance does not cover the amount, the request is refused.
- Once the refund is confirmed, the original payment moves to status REFUNDED.
No phone number required
Initiate a refund
POST/api/v1/payments/:id/refund
Authentication
kpay_test_xxxxxxxxxxxxxxxx. Use your test keys: no real money moves.Path parameter
idstringrequisKPay identifier of the payment to refund, as returned when the payment was initiated.
Request body
reasonstringReason stored in the transaction history and echoed in webhooks. 255 characters maximum.
externalIdstringYour own refund identifier. Acts as an idempotency key: replaying the request with the same value will not trigger a second refund. Generated by KPay if omitted. 100 characters maximum.
const res = await fetch("https://test.admin.kpay.site/api/v1/payments/a1b2c3d4-5e6f-7890-1a2b-3c4d5e6f7081/refund", {
method: "POST",
headers: {
"X-API-Key": process.env.KPAY_API_KEY,
"X-Secret-Key": process.env.KPAY_SECRET_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
"reason": "Produit en rupture de stock",
"externalId": "REFUND-CMD-2026-00042"
}),
});
const data = await res.json();{
"id": "b7c1e2f3-4a5d-6e7f-8901-2b3c4d5e6f70",
"status": "PENDING",
"amount": 5000,
"currency": "XAF",
"originalPaymentId": "a1b2c3d4-5e6f-7890-1a2b-3c4d5e6f7081",
"originalPaymentStatus": "COMPLETED",
"message": "Remboursement initié — transfert en cours vers le payeur"
}Asynchronous processing
Tracking a refund
The final status is delivered to the refund callback URL configured on your application.
Events
refund.completedwebhookThe refund succeeded. Funds left your wallet and the original payment becomes REFUNDED.
refund.failedwebhookThe refund failed. Reserved funds are released and the original payment stays COMPLETED.
Without a webhook, poll GET /api/v1/payments/:id.
Errors
In sandbox
Refunds behave identically in sandbox and production. First capture a payment using a test number that completes, then refund it: no real money moves.