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 operator that serves the number, such as cm.mtn or cm.orange. See below for the generic form.
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.kZ3qP8mWvL2xR7tB5nY4hC6dF9jS1aG0eU3i…" \
  -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",
"payment_method": {
"id": "pm_4Dw8kR2xN6vQ",
"type": "mobile_money",
"name": "MTN Mobile Money"
},
"created_at": "2026-09-11T14:00:00Z",
"updated_at": "2026-09-11T14:00:00Z"
}
}

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.

The channel decides the operator

Pass the operator directly when you know it, cm.mtn or cm.orange. Pass the country's generic channel, cm.mobile, when you do not, and the operator is worked out from the number itself.

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

Response · 422 when the number matches no operator
{
"message": "Could not determine operator from phone number for cm.mobile.",
"errors": {
"channel": [
"Could not determine operator from phone number for cm.mobile. Use a valid number (e.g. MTN +23767..., Orange +23769...) or specify the channel explicitly (e.g. cm.mtn, cm.orange)."
]
}
}

If you already know the operator, name it. The generic form is there for the case where you hold a 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 — 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 is simulated like any other: it succeeds, fails or stalls on the scenario you asked for, and 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.kZ3qP8mWvL2xR7tB5nY4hC6dF9jS1aG0eU3i…"

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 it goes through the search index, which 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
privilegedWajub, in the back officeOnly once Wajub verifies it

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.

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.

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. Not on the duplicate return above, which creates nothing
beneficiary.updatedOne of the five editable fields changes
beneficiary.deletedA beneficiary is deleted

What did you think of this content?