Skip to content
Loading keys…

Failure Reasons

Reading failure_reason on a failed payment, transfer or refund.

A 4xx means the request was refused. This page is about the other kind of failure: a request that succeeded, created a resource, and then failed later at the operator. The object and its webhook carry two fields that explain it.

failure_reasonstringoptional
A short machine-readable code. Branch on this one. Present only once the object reaches status: "failed".
failure_messagestringoptional
One sentence for a human. Show it in your back office, log it, never branch on it. Falls back to the code when the provider gave no text.

Where it appears

A failed transfer
{
"id": "po_225KMKULRQqYvlhrgUOE",
"status": "failed",
"amount": 100000,
"currency": "XAF",
"reason": "Salaire septembre",
"failure_reason": "invalid_recipient",
"failure_message": "Beneficiary has no payout channel."
}

The same pair is delivered in the payment.failed, transfer.failed and refund.failed webhooks, since those payloads are the resource itself.

ObjectField carriedSet when
Transferfailure_reason, failure_messageOnly while status is failed.
Refundfailure_reason, failure_messageOnly while status is failed.
Paymentfailure_reason, failure_messageOnly while status is failed. See Failed payments.

Codes Wajub issues itself

These are stable and exhaustive. They are decided before, or instead of, any call to an operator, which is why Wajub controls their spelling.

Transfers

failure_reasonWhat happenedWhat to do
team_restrictedPayouts are suspended on your account.Contact support. Do not retry.
invalid_recipientThe beneficiary is missing, or has no payout channel.Fix the beneficiary, then create a new transfer.
no_providerNo provider is eligible for that channel and amount right now.Retry later, or route to another channel.
provider_rejectedEvery eligible provider refused the payout.Read failure_message, fix, then create a new one.
admin_rejectedWajub declined it after a review.Contact support. Do not retry.
merchant_rejectedYou rejected it from the Dashboard before it was sent.Nothing: you stopped it. Do not retry.
cancelled_by_wajubWajub cancelled it before it was sent.Contact support. Do not retry.

The last two come on a cancelled payout from API version 2026-10-12, and on a failed one before.

Refunds

A failed refund always carries one of these, live and sandbox alike. When the provider reports the failure, Wajub maps it to one of these codes; failure_message keeps the provider's sentence when it sent one.

failure_reasonWhat happenedWhat to do
insufficient_fundsThe provider's float could not cover the refund.Retry later, once.
refund_rejected_by_issuerA card issuer refused it.Pay the customer back another way.
transaction_already_reversedThe payment was already reversed or refunded at the provider.Check the Dashboard before acting.
invalid_numberThe payer's number or account cannot receive it. Live only.Ask the customer for another way to pay them back.
limit_exceededA limit on the receiving account or at the provider was reached. Live only.Retry later, once.
network_errorThe provider could not be reached in time.One retry is reasonable.
provider_errorThe provider was down, or refused the call itself.Wait, then retry once.
provider_declinedThe provider refused it without saying why.Read failure_message, then decide.
ops_failedWajub could not complete the refund and closed it, for no known reason. A refund the provider had refused ends with the provider's reason above instead. Live only.Contact support before refunding again.
cancelled_by_wajubWajub cancelled the refund before sending it: nothing left your balance. The refund is cancelled from API version 2026-10-12, failed before. Live only.Refund again if the customer is still owed.

A refund that cannot be completed automatically is not dropped: Wajub completes it, and it stays pending (or processing) in the meantime. failed is the end of that road, not the start. A retry is a new refund.

Failed payments

A failed payment carries the reason of its last failed attempt, on the payment and in the payment.failed webhook:

failure_reasonWhen
The provider's own codeThe provider refused the attempt with a code. It is passed through unchanged, see Codes the operator issues, and failure_message carries the provider's sentence.
payment_failedThe attempt failed without a provider code, a timeout or an error on our side of the call. failure_message is The payment could not be completed.
declined, authentication_failedSandbox only, see Sandbox is deterministic.

Older failed payments may carry neither field.

The status still separates the three cases that change what you do next:

statusWhat happened
failedThe operator refused the charge.
expiredThe customer never validated in time and the session lapsed.
cancelledThe customer, or your own DELETE /payments/{id}, stopped it.

For every attempt, not only the last, open the payment in Konsole: its timeline shows each attempt, the provider involved, and the raw gateway response. That is also where the dashboard reads from when it prints a plain-language explanation next to a failed payment.

Codes the operator issues

Anything not in the tables above is the provider's own error code, passed through unchanged. There is no shared vocabulary between operators: MTN, Orange, Wave and a card acquirer each name the same customer mistake differently, and a new provider can introduce a new code without a release on our side.

const RETRY_NEVER = new Set([
'team_restricted',
'admin_rejected',
'merchant_rejected',
'invalid_recipient',
]);

function onTransferFailed(transfer) {
const reason = transfer.failure_reason;

if (RETRY_NEVER.has(reason)) {
  return escalateToSupport(transfer);
}

if (reason === 'no_provider') {
  return scheduleRetry(transfer, { in: '15m' });
}

return notifyMerchant(transfer.failure_message);
}

Sandbox is deterministic

The sandbox calls no operator: the test number, account or address you use decides the outcome, so every branch of your handler can be exercised on purpose before you go live. The tables of test numbers are in Test scenarios.

Transfers. The last six digits of a Mobile Money test number (and the suffix of a bank test account or a crypto test address) pick the result. A transfer that fails carries one of three reasons:

SuffixResultfailure_reason
000000succeeded
000001Refused at once with a 422, error_code: insufficient_funds; no transfer is created.
000002failedfailure
000003failednetwork_error
000004failedinvalid_recipient

A number that is not a test number is refused with a 422 that lists the ones to use.

Refunds. The payment method of the refunded payment decides: a Mobile Money payment paid with a number ending in 000000 is refunded, one ending in 000001 to 000004 or 000009 fails; the card 4000 0000 0000 5423 is paid but its refunds always fail; bank and crypto payments are always refunded. Only a payment with no payment method attached is left to chance, 70 % success. The failure_reason of a failed sandbox refund is drawn from the same list as live, in Refunds.

Payments. The last six digits of the test number decide the status: 000002 ends failed, 000003 ends expired, 000004 ends cancelled. 000001 is refused at once with a 402 and error_code: insufficient_funds, as a live decline is, and the payment stays pending. A sandbox payment that fails says why with a fixed pair, chosen by the test number or card you paid with:

Scenariofailure_reasonfailure_message
Failure (…000002, a declining card)declinedThe operator declined the payment.
The issuer declines after 3D SecuredeclinedThe issuer declined the card after authentication.
The payer fails 3D Secureauthentication_failedThe payer did not pass 3D Secure authentication.

A failure is not always the end

A no_provider or a network error leaves your balance untouched: the reservation is released when the transfer fails. Check the balance rather than assuming, then create a new transfer with a new idempotency key.

What did you think of this content?