Refunds
Refund all or part of a confirmed payment, choose who pays the fee and set the project default.
A refund sends money back to the customer, on the mobile number that paid. It is taken from the merchant's available balance. It happens in two steps: a quote (amounts frozen, no money movement), then the refund.
Conditions
- The payment is
confirmed. - The payment was created with
customer_phone: without the customer number, no refund is possible. - The amount does not exceed what is left to refund (payment amount minus
refunds done or in progress). Partial refunds are allowed. Beyond that:
400 INVALID_REQUESTwithfields.amount, which gives the refundable remainder. - The available balance covers the debited amount (
merchant_debited).
No identity check is required to refund.
1. Get a quote
curl -X POST "$API_URL/v1/refund-quotes" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"payment_id": "8d1f0c6e-3b7a-4c55-9a51-2f0e5c1d7a90",
"amount": "5000",
"fee_payer": "merchant"
}'{
"payment_id": "8d1f0c6e-3b7a-4c55-9a51-2f0e5c1d7a90",
"amount": "5000",
"fee": "…",
"customer_receives": "5000",
"merchant_debited": "…",
"fee_payer": "merchant",
"quote_hash": "a3f9…",
"expires_at": "2026-10-07T10:02:00Z"
}The quote expires after 2 minutes. Show it before confirming: it gives the exact amounts.
Who pays the fee: fee_payer
fee_payer | The customer receives | The merchant is debited |
|---|---|---|
merchant | amount | amount + fee |
customer | amount − fee | amount |
Without fee_payer, the project default applies.
2. Refund
Reuse the quote values, add reason and an Idempotency-Key:
curl -X POST "$API_URL/v1/refunds" \
-H "Authorization: Bearer $API_KEY" \
-H "Idempotency-Key: refund-order-1042" \
-H "Content-Type: application/json" \
-d '{
"payment_id": "8d1f0c6e-3b7a-4c55-9a51-2f0e5c1d7a90",
"amount": "5000",
"fee_payer": "merchant",
"reason": "customer_request",
"quote_hash": "a3f9…"
}'reason | Meaning |
|---|---|
customer_request | Customer request. |
duplicate | Duplicate payment. |
product_not_delivered | Product or service not delivered. |
other_documented | Other documented reason. |
A quote is single use. Expired: 409 QUOTE_EXPIRED. Unknown, already used or
not matching the request (another amount, another fee_payer):
409 QUOTE_INVALID. The message is also in fields.quote_hash. Request a new
quote. IDEMPOTENCY_CONFLICT now only means the Idempotency-Key reused with
another body.
Response: 201 on creation, 200 for a request replayed with the same key
and the same body.
{
"id": "c27e5a10-6f8b-4d2c-b1e3-7a9d0f4c8e21",
"payment_id": "8d1f0c6e-3b7a-4c55-9a51-2f0e5c1d7a90",
"status": "processing",
"amount": "5000",
"currency": "XOF",
"fee": "…",
"customer_receives": "5000",
"merchant_debited": "…",
"fee_payer": "merchant",
"reason": "customer_request",
"created_at": "2026-10-07T10:01:00Z",
"updated_at": "2026-10-07T10:01:01Z"
}Statuses
| Status | Meaning | Webhook event |
|---|---|---|
requested | Refund recorded, funds being reserved. | refund.requested |
processing | Payment to the customer in progress. | None |
succeeded | The customer received the amount. | refund.succeeded |
failed | Payment declined; the reserved funds return to the available balance. | refund.failed |
unknown | Uncertain outcome; the funds stay reserved until the operator answers, never released by default. | None |
Read and list
curl "$API_URL/v1/refunds/$REFUND_ID" -H "Authorization: Bearer $API_KEY"
curl "$API_URL/v1/refunds?payment_id=$PAYMENT_ID" -H "Authorization: Bearer $API_KEY"Filters: payment_id, status, from, to, limit, offset.
GET /v1/payments/{id} also lists the payment's refunds in refunds.
Project default
curl "$API_URL/v1/settings" -H "Authorization: Bearer $API_KEY"
curl -X PUT "$API_URL/v1/settings" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"refund_fee_payer_default": "customer"}'refund_fee_payer_default is merchant or customer. Every change is
logged. The same setting is available in the dashboard, Refunds section.
Refund fee rate: see Fees.