Skip to content

Refunds

Return funds on a succeeded payment, and follow the refund to its own outcome.

A refund is its own object, with its own id, its own status and its own webhooks. It is not a state the payment moves into. That distinction matters more than it sounds: the payment you refund stays succeeded for the rest of its life, and everything you want to know about the money coming back lives on the refund instead.

You can return the full amount or part of it, and stack several partial refunds until the original amount is exhausted.

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.

ConditionWhat you get back when it fails
The payment is succeededTransaction must be completed to be refunded. Current status: …
Something is still refundableTransaction has already been fully refunded.
The amount fits what is leftRefund amount exceeds refundable amount (…)
The currency matches the paymentRefunds 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 pending refund therefore reserves its share straight away, which is what stops two concurrent calls from refunding the same money twice.

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.

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_modeWho sends it
automaticWajub, back to the payer's mobile money number, within minutes
merchantYou: the payment was collected on your own provider account, which Wajub cannot refund from
manualWajub support

manual_reason is an object with a stable code to branch on and a message you can show.

codeWhat it means
refund_from_your_providerCollected on your own provider account: refund it there. It updates here once the provider confirms
card_or_bank_paymentAutomatic refunds cover mobile money. The Wajub team sends this one
insufficient_balanceYour balance did not cover the refund when it was due to be sent
payer_number_missingThe payer's mobile money number is not on record
requested_over_24hRequested more than a day ago: the Wajub team checks it was not refunded another way first
outcome_being_confirmedSent, and the operator's answer is not final: the Wajub team confirms whether the money arrived
automatic_attempt_failedThe operator refused it and no money left: Wajub support processes it
wajub_reviewAnything else the Wajub team has to look at first

A refund can change lane without changing status. One not yet sent stays pending when the Wajub team takes it over. One already handed to the operator stays processing when the operator refuses it and Wajub support processes it (automatic_attempt_failed): it does not go back to pending. Either way it ends succeeded or failed. Each change of lane sends refund.updated with the refund as it now stands. Sandbox refunds have neither field.

What the payment's status says

It depends on your API version, and this is the part that catches people.

From version 2026-10-11, 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. On an earlier version the payment keeps reading succeeded, however much was refunded. Versioning says which one your account is pinned to.

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, on any version, list the payment's refunds and add up the ones that are pending, processing or succeeded.

GEThttps://api.wajub.com/payments/{id}/refunds

Subtract 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.

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 byfees_bearer: merchantfees_bearer: customer
You (bearer: merchant on the payment)The customer gets the amount back. The fee you paid is not returned to youThe 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 bothThe 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 and how refunds have always worked. 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.

What did you think of this content?