Skip to content

Beneficiaries

Save a payout destination once, then pay it by id instead of by number.

A beneficiary is a destination you have saved: a name, a Mobile Money number, and the operator that serves it. You create it once and pay it by id afterwards, which keeps the number out of every payout call you make and out of the logs those calls leave behind.

It is also the only way to pay the same person twice without retyping where the money goes, and retyping is how money reaches the wrong phone.

Creating one

POSThttps://api.wajub.com/beneficiaries
channelstringrequired
The channel of the operator that serves the number, such as cm.mtn or ke.mpesa. In Cameroon only, cm.mobile lets the number decide: see below.
namestringrequired
The payee, as it should appear on the payout. Also used as the account holder name.
phonestringoptional
The number that receives the money. Required unless you send account_number. The field is phone, not phone_number.
account_numberstringoptional
The destination, when it is not a phone number. One of the two is required, and phone fills this in when you omit it.
emailstringoptional
Contact address for the payee. Not used to send anything today.
country_codestringoptional
Two letters. Defaults to the first two of the channel slug, so cm.mtn gives CM.
notestringoptional
Free text for your own reference, up to 1 000 characters.
metadataobjectoptional
Your own key and value pairs, returned unchanged on every read.

Send it with the secret key. Beneficiary creation carries its own tighter rate limit than the rest of the API, since adding a destination is half of what an attacker needs to move money.

curl https://api.wajub.com/beneficiaries \
  -H "Authorization: sk.xxxxxxxxxxxxxxxx…" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: beneficiary-amina" \
  -d '{
    "channel": "cm.mtn",
    "name": "Amina Traoré",
    "phone": "+237670000000"
  }'

The answer is 201 Created. The id is what every payout takes, prefixed ben_ in live and ben_test_ in sandbox, so the two environments can never be confused for one another.

Response · 201 Created
{
"code": 201,
"status": "Created",
"message": "Beneficiary created successfully",
"beneficiary": {
"id": "ben_7Kq2mX9vL4tRb3nP8sZc",
"name": "Amina Traoré",
"phone": "+237670000000",
"country_code": "CM",
"note": "Retainer, paid on the 1st",
"metadata": [
],
"sandbox": false,
"is_active": true,
"type": "standard",
"verified": true,
"payment_method": {
"id": "pm_4Dw8kR2xN6vQ",
"type": "mobile_money",
"name": "MTN Mobile Money"
},
"created_at": "2026-09-11T14:00:00+00:00",
"updated_at": "2026-09-11T14:00:00+00:00"
}
}

The destination is not in the response

payment_method carries an id, a type and a label, never the number the money goes to. Nothing on the API reads a beneficiary's destination back, so if you need to show a payee where their money is being sent, keep your own copy at creation time. Fields you left empty are dropped from the response rather than returned as null, and an empty metadata comes back as [] rather than {}, so type it as a map that can arrive as an empty array.

In sandbox, test numbers only

A sandbox beneficiary must already carry a test number when you create it: anything else answers 422 on phone and account_number with Invalid sandbox test number. Use a Mobile Money test number, e.g. +237670000000 (success), +237670000001 (insufficient funds), +237670000002 (failure), +237670000003 (timeout), +237670000004 (canceled). Load real numbers in live only. The test numbers of every operator are on Test scenarios.

The channel decides the operator

Name the operator's channel: ke.mpesa, eg.vodafone, cm.mtn, cm.orange. Cameroon is the one exception: when you hold a Cameroonian number but not its operator, pass cm.mobile, and the operator, cm.mtn or cm.orange, is worked out from the number itself. In live it comes from the country's numbering plan, as it does for a payment; in sandbox it comes from the test prefixes. No other country has a generic channel: ke.mobile or any other {country}.mobile is refused as an unknown channel.

That resolution can fail, and it fails loudly rather than picking one at random.

Response · 422 when the number matches no operator
{
"code": 422,
"status": "Unprocessable Content",
"message": "Could not determine operator from phone number for cm.mobile. Use a valid number of that country or specify the channel explicitly (e.g. cm.mtn, cm.orange). (and 1 more error)",
"errors": {
"channel": [
"Could not determine operator from phone number for cm.mobile. Use a valid number of that country or specify the channel explicitly (e.g. cm.mtn, cm.orange)."
],
"phone": [
"Could not determine operator from this phone number for cm.mobile."
]
}
}

If you already know the operator, name it. cm.mobile is there for the case where you hold a Cameroonian number and nothing else.

Paying another Wajub account

When the person you pay already has a Wajub account, use the wajub channel and their 11-digit account number. The money never leaves Wajub: one balance is debited and the other credited, with no network in between and nothing to wait for.

A beneficiary on another Wajub account
{
"name": "Boutique Ada",
"channel": "wajub",
"account_number": "12345678901"
}

The account has to exist, it cannot be your own, and it cannot be an account Wajub has frozen: each is refused with 422 when you create the beneficiary, and a payout to an account frozen since then fails on its own. Paying it is an ordinary transfer, with the same limits and the same statuses; it simply succeeds at once, and it is free, since there is no network to pay between two Wajub accounts. On an account whose beneficiaries Wajub checks, the name is matched against the recipient's compliance file instead of an operator's records.

In sandbox

A sandbox payout on this channel always succeeds: the test scenarios do not apply to it, so a failure on this channel can only be seen in live. The recipient's sandbox balance is never credited. Only live moves money between accounts.

Paying one

Pass the id where a payout expects a recipient. Everything else about the transfer is unchanged.

POSThttps://api.wajub.com/transfers

The body is the same one Transfer reference documents, with a string where the inline object would have been.

Pay a saved beneficiary
{
"beneficiary": "ben_7Kq2mX9vL4tRb3nP8sZc",
"amount": 15000,
"currency": "XAF",
"description": "September retainer"
}

Saving a payee does not make paying them safe

A beneficiary is a destination, not a guarantee against duplicates. Every POST /transfers still needs its own Idempotency-Key, saved payee or not, or a retried request pays twice. See Idempotency.

What you can change, and what you cannot

PUThttps://api.wajub.com/beneficiaries/{id}

The update accepts five fields, and the destination is not among them.

FieldEditable
name, email, country_code, metadataYes
phoneYes, as a contact detail only
channel, account_number, the payout destinationNo

Retrieving one

GEThttps://api.wajub.com/beneficiaries/{id}

The same object creation returned, read back by its id. Use it to check a payee is still active before a payout, and remember it still will not tell you the number the money goes to.

curl https://api.wajub.com/beneficiaries/ben_7Kq2mX9vL4tRb3nP8sZc \
  -H "Authorization: sk.xxxxxxxxxxxxxxxx…"

A 404 covers three different situations without distinguishing them: the id does not exist, it belongs to another account, or it belongs to the other environment. A ben_test_ id read with a live key is the common one.

Listing them

GEThttps://api.wajub.com/beneficiaries

Results come back newest first, twenty-five at a time unless you raise per_page, which caps at 100. The envelope is items and meta, and passing cursor switches the pagination mode and the shape of meta with it, the same way it does on transfers.

Response · 200 OK
{
"code": 200,
"status": "OK",
"message": "Beneficiaries retrieved",
"items": [
{
"id": "ben_7Kq2mX9vL4tRb3nP8sZc",
"name": "Amina Traoré",
"type": "standard",
"is_active": true
}
],
"meta": {
"current_page": 1,
"per_page": 25,
"total": 1
}
}

search does not behave the same in both environments. In sandbox it is a direct match on the name, the email, the phone and the id. In live, search also covers the country code and the type, so a live search returns results a sandbox search would not.

is_active filters on the flag. type is the filter worth understanding.

TypeCreated byVerified
standardYour own API callsOn creation, except on a Relay account
privilegedYou, in the Dashboard, as a payout accountOnly once you verify it there

The list returns standard only unless you ask otherwise, and that default is deliberate. A privileged beneficiary can sit unverified, and a live payout to an unverified destination is refused with This beneficiary payment method has not been verified yet and cannot receive live payouts. Pass type=privileged or type=all when you genuinely want to see them.

Your own beneficiaries are marked verified the moment they are created, because the merchant vouches for its own payees. That is what makes a standard beneficiary payable straight away. On a Relay account, the seller's wallet is first matched against the name its operator holds, and Paying sellers covers that case.

Checking the name on a wallet

POSThttps://api.wajub.com/beneficiaries/{id}/verify

The call takes no body. It asks the operator whose name the wallet is registered under and compares it with the name you declared. The comparison ignores word order, accents and punctuation, and tolerates small spelling differences.

On your own beneficiaries the answer is information. The beneficiary stays payable whatever comes back, the call answers 200, and registered_name carries the name the operator holds, so you can show it to whoever is about to send the money.

verificationWhat happenedregistered_name
verifiedThe operator's name matches the one you declaredPresent
name_mismatchThe operator holds the number under another namePresent
lookup_unavailableThe operator could not be asked: it offers no lookup on this channel, it did not answer, or it does not know the numberAbsent
not_foundThe beneficiary has no payout destination to check. The call answers 422 with This beneficiary has no payout destination to verify.Absent
Response · 200 when the operator holds another name
{
"code": 200,
"status": "OK",
"message": "The operator holds this number under another name. The beneficiary can still be paid.",
"verification": "name_mismatch",
"registered_name": "Paul Nkolo",
"beneficiary": {
"id": "ben_7Kq2mX9vL4tRb3nP8sZc",
"name": "Amina Traoré",
"verified": true,
"verification": "name_mismatch"
}
}

The operators that answer today are MTN, Orange Money and Express Union in Cameroon, cm.mtn, cm.orange and cm.eu, and a Wajub account, wajub. Every other channel returns lookup_unavailable.

The beneficiary keeps the last outcome: verification carries it on every read, and is absent until a check has run. verified is a different thing. It says whether Wajub will pay the beneficiary in live mode, and here a name_mismatch does not change it. One exception: a beneficiary left unverified, a seller the operator could not confirm while your account was on Relay for instance, becomes payable when the names match, and is then paid under the name the operator holds. A mismatch or an operator that cannot answer leaves it unpayable.

The call has a ceiling of its own, whatever your plan: 10 checks a minute and 200 a day per account in live, 10 a minute in sandbox. Past it the answer is 429. It is meant for your own payees, checked as you add them, not for looking numbers up in bulk.

Where the check decides instead of informing

On a Relay account, and on any account Wajub has set to require it, the same call is a gate. A beneficiary already verified is returned as it is, anything other than verified answers 422, the beneficiary stays unpayable, and registered_name is not returned. In sandbox there is no operator to ask, so a beneficiary on such an account is created verified and the call answers verified: your handling of a name_mismatch can only be exercised in live. A payout account you added in the Dashboard is never verified by this call, on any account: verify it there.

Payout accounts

A payout account is where your own money goes: the destination of the daily settlement and of the payouts you make to yourself. You create it under Settings, Transfers, Payout Accounts, on Mobile Money, Djamo, a Wajub account, or a bank account through wire. It is stored as a privileged beneficiary and starts unverified. Verifying it from the same page matches the account against the name its operator holds. If the names cannot be matched, Wajub asks the account owner to confirm the account. Wajub processes wire payouts, for a fixed fee, within 48 business hours by default.

Statements and debts

Three more routes hang off a beneficiary.

RouteWhat it does
GET /beneficiaries/{id}/statementEverything paid to them over a period of up to a year, split into paid, in progress and failed
POST /beneficiaries/{id}/receivablesRecord what they owe you back after a refund. GET on the same path lists those debts
POST /beneficiaries/{id}/receivables/{receivable}/settleMark a debt as recovered outside Wajub

A recorded debt sends beneficiary.receivable_recorded. With netting on for your account, the next batch payout to that beneficiary is reduced by it and sends beneficiary.receivable_recovered. Paying sellers walks through both.

Deleting one

DELETEhttps://api.wajub.com/beneficiaries/{id}

The answer is 200. The record stops appearing in lists and can no longer be paid, while transfers that already went to it keep pointing at it, so your history stays readable: their beneficiary carries deleted: true, and GET /beneficiaries/{id}/statement still answers for it.

There is no restore on the API. Creating the same destination again gives you a new beneficiary with a new id, not the old one back.

What reaches your webhook

EventFires when
beneficiary.createdA record is actually written: by POST /beneficiaries, inline by POST /transfers, or as a payout account in the Dashboard. Not on the duplicate return above, which creates nothing
beneficiary.updatedOne of the five editable fields changes
beneficiary.deletedA beneficiary is deleted
beneficiary.receivable_recordedA debt is recorded against a beneficiary
beneficiary.receivable_recoveredA batch payout recovered part or all of a debt

What did you think of this content?