Refunds
Return funds on a succeeded payment, and follow the refund to its own outcome.
A refund returns all or part of a succeeded payment, and you can stack several partial refunds until the original amount is exhausted. It is its own object, with its own id, status and webhooks.
Refunds require the secret key
POST /refunds requires a secret key. A call carrying a public key is refused with
406 Not Acceptable and the message Private Key Required. This endpoint belongs on your
server, never in a browser or a mobile app.
Two pages go further:
- Create a refund, the call, its parameters and its answer.
- Track a refund, its webhooks, why one fails, how to test a failure, and the list of refunds.
What can be refunded
Four conditions are checked before a refund is recorded, and each one fails with its own 422.
| Condition | What you get back when it fails |
|---|---|
The payment is succeeded | Transaction must be completed to be refunded. Current status: … |
| Something is still refundable | Transaction has already been fully refunded. |
| The amount fits what is left | Refund amount exceeds refundable amount (…) |
| The currency matches the payment | Refunds must be in the transaction currency (XAF). |
The refundable amount is the payment amount minus everything already refunded, counting refunds
that are still pending as well as those that succeeded.
A refund cannot convert a currency
currency is accepted, but the only value it may hold is the payment's own currency. It exists
so you can be explicit, not so you can refund a XAF payment in XOF. Omit it and Wajub uses the
payment's currency anyway.
The refund has its own lifecycle
Five states, three of them terminal. A refund never returns to an earlier one.
How long that takes is not up to Wajub. On a card the issuer decides, and several days is normal. On Mobile Money the credit usually lands in minutes, sometimes hours. Nothing about your integration changes that, so tell the customer a range rather than a time.
A refund starts in a different state depending on the environment
In production a refund is created pending and moves to processing when it is sent. In
sandbox it is created processing directly and resolves within seconds. Write your handler against the terminal states and you will not notice
the difference; branch on pending and your sandbox tests will never hit that branch.
Who sends a live refund
In production, a refund is sent one of three ways. execution_mode on the refund says which, once
Wajub has planned it, and manual_reason says why when it is not sent automatically.
execution_mode | Who sends it |
|---|---|
automatic | Wajub, back to the payer's mobile money number, within minutes |
merchant | You: the payment was collected on your own provider account, which Wajub cannot refund from |
manual | Wajub support |
manual_reason is an object with a stable code to branch on and a message you can show.
code | What it means |
|---|---|
refund_from_your_provider | Collected on your own provider account: refund it there. It updates here once the provider confirms |
card_or_bank_payment | Automatic refunds cover mobile money. Wajub support sends this one |
insufficient_balance | Your balance did not cover the refund when it was due to be sent |
payer_number_missing | The payer's mobile money number is not on record |
requested_over_24h | Requested more than a day ago: Wajub support sends it |
outcome_being_confirmed | Sent, and the operator's answer is not final yet |
automatic_attempt_failed | The operator refused it and no money left: Wajub support processes it |
wajub_review | Wajub support handles it |
execution_mode and manual_reason can change without the status changing; each change sends
refund.updated with the refund as it now stands. Either way the refund ends succeeded or
failed. Sandbox refunds have neither field.
What the payment's status says
A payment reads partially_refunded once refunds that succeeded cover part of its amount, and
refunded once they cover all of it. A refund that is pending, processing or failed changes
nothing.
The status is a reading and nothing else moves with it: no payment event is sent, and the payment
is otherwise untouched. To know what is still refundable, list the payment's refunds and add up the
ones that are pending, processing or succeeded.
https://api.wajub.com/payments/{id}/refundsSubtract that total from the payment amount and you have what is still refundable, which is exactly the figure the API checks against when you post the next refund.
Filtering refunded payments
GET /payments?status=refunded and ?status=partially_refunded list those payments, and
?status=succeeded leaves them out.
Who bears the fee
Wajub keeps the fee of a payment you refund. What you decide is who ends up bearing it, for the
share of the fee that matches the refunded amount: you, or the customer. That is fees_bearer.
| The fee was paid by | fees_bearer: merchant | fees_bearer: customer |
|---|---|---|
You (bearer: merchant on the payment) | The customer gets the amount back. The fee you paid is not returned to you | The fee share is deducted from what the customer gets back, and you are debited that much less |
The customer, on top (bearer: customer) | The customer gets the amount and the fee share back, and you are debited both | The customer gets the amount back, not the fee |
Left out, the fee stays with whoever paid it, which is the first and the last cell of the table. To
change that for every refund, set it once in Settings,
Refunds; fees_bearer on one refund overrides it.
The refund tells you what was applied: fee_amount is the fee share, and payer_amount what the
customer receives. Your balance is debited payer_amount, never more and never less. A partial
refund takes the same proportion of the fee, and the refund that completes the payment takes what
is left of it, so the shares add up exactly. In a currency with no subunit, such as XAF, a share is
a whole number, so payer_amount is too.
A payment collected on your own provider account is different. Wajub holds none of its money, so
nothing is debited from your balance: you send the refund yourself, from that provider, and the
amount to send is payer_amount. With bearer: merchant Wajub charged no fee on it, so
fee_amount is 0 and payer_amount equals amount. With bearer: customer the payer paid the
fee on top to your provider, and the table above applies to it.
The money moves on success, not on creation
Creating a refund reserves the amount against what is refundable, but touches no balance. The debit
happens when the refund reaches succeeded, and only then, for payer_amount: the amount, give or
take the fee share.
Two consequences worth planning around. In sandbox, a refund that succeeds debits your sandbox balance by the amount refunded, as a live one debits your balance. A refund that pays back a dispute you accepted debits it even when it fails, since the dispute is lost either way. And a refund issued on a payment you already lost to a dispute is capped at whatever principal has not already been reversed, so the same money cannot leave twice through two different doors.
Where the balance goes afterwards, and how a negative one behaves, is covered in Balance and settlements.
In the Dashboard
A refund can be issued without any code. Open a payment under Payments, use its refund action,
and the result is the same object the API creates. The form asks who bears the fee when the payment
carried one, and shows what the customer will receive. The action needs the refund_payments
permission, which is separate from manage_payments, so a teammate can manage orders without being
able to give money back.
Refunds lists them all with their status, and exports to CSV or Excel in production.
Refund requests is the inbound side. A payer using Pocket can ask for a refund on a payment they made, and the request waits there until someone approves or rejects it. Nothing is refunded until it is approved, and approving creates the refund exactly as the API would.
Related pages
- Handling refundsThe full journey, reconciliation included.
- DisputesWhen the money is taken back rather than given back.
- Balance and settlementsWhere a refund lands once it has succeeded.
- Payment lifecycleWhy the payment stays succeeded through all of this.
- Sandbox modeTest numbers and cards, and what each one triggers.
- Refunds API referenceEvery field on the resource, and the three endpoints.