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.
Mobile Money, Djamo or a Wajub account, whatever the fields suggest
Creation accepts iban, swift and other bank fields, but the channel you pass has to be one
of the fifty Mobile Money channels that can pay out, listed on Providers &
channels, a Djamo wallet (ci.djamo or sn.djamo), or wajub for
another Wajub account. Anything else answers 422 with The selected channel is invalid. (The selected beneficiary.channel is invalid. for a beneficiary sent inline with a transfer). Bank destinations exist as payout accounts, which you create in the
Dashboard rather than through the API.
Creating one
https://api.wajub.com/beneficiarieschannelstringrequiredcm.mtn or ke.mpesa. In Cameroon only, cm.mobile lets the number decide: see below.namestringrequiredphonestringoptionalaccount_number. The field is phone, not phone_number.account_numberstringoptionalphone fills this in when you omit it.emailstringoptionalcountry_codestringoptionalcm.mtn gives CM.notestringoptionalmetadataobjectoptionalSend 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.
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.
If you already know the operator, name it. cm.mobile is there for the case where you hold a
Cameroonian number and nothing else.
The same destination twice returns the first one
Creation is keyed on the destination, not on what you send with it. If an active beneficiary
already exists for that number on that channel, the call returns that one unchanged, with the same
201 Created and the same success message as a real creation. The name, email and note you
just sent are discarded. Read the id that comes back rather than assuming it is new, and never
treat a 201 here as proof that a fresh record was written.
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.
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.
https://api.wajub.com/transfersThe body is the same one Transfer reference documents, with a string where the inline object would have been.
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
https://api.wajub.com/beneficiaries/{id}The update accepts five fields, and the destination is not among them.
| Field | Editable |
|---|---|
name, email, country_code, metadata | Yes |
phone | Yes, as a contact detail only |
channel, account_number, the payout destination | No |
Changing phone does not change where the money goes
The destination is fixed on the payment method attached when the beneficiary was created, and
nothing on the API touches it afterwards. Updating phone rewrites the contact number on the
record while the payout keeps going to the original one, with no warning and no difference in the
response. To pay a different number, create a second beneficiary and use its id.
Retrieving one
https://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
https://api.wajub.com/beneficiariesResults 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.
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.
| Type | Created by | Verified |
|---|---|---|
standard | Your own API calls | On creation, except on a Relay account |
privileged | You, in the Dashboard, as a payout account | Only 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
https://api.wajub.com/beneficiaries/{id}/verifyThe 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.
verification | What happened | registered_name |
|---|---|---|
verified | The operator's name matches the one you declared | Present |
name_mismatch | The operator holds the number under another name | Present |
lookup_unavailable | The operator could not be asked: it offers no lookup on this channel, it did not answer, or it does not know the number | Absent |
not_found | The beneficiary has no payout destination to check. The call answers 422 with This beneficiary has no payout destination to verify. | Absent |
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.
Sandbox asks no operator
On your own beneficiaries, a sandbox key always gets lookup_unavailable: there is no operator
behind a test number. The name is only checked in live mode.
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.
| Route | What it does |
|---|---|
GET /beneficiaries/{id}/statement | Everything paid to them over a period of up to a year, split into paid, in progress and failed |
POST /beneficiaries/{id}/receivables | Record what they owe you back after a refund. GET on the same path lists those debts |
POST /beneficiaries/{id}/receivables/{receivable}/settle | Mark 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
https://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
| Event | Fires when |
|---|---|
beneficiary.created | A 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.updated | One of the five editable fields changes |
beneficiary.deleted | A beneficiary is deleted |
beneficiary.receivable_recorded | A debt is recorded against a beneficiary |
beneficiary.receivable_recovered | A batch payout recovered part or all of a debt |
Related pages
- Transfers & PayoutsWhere payouts leave from, and what has to be true first.
- TransfersThe payout itself: statuses, routing, and failures.
- Transfers QuickstartSend your first payout to a beneficiary.
- IdempotencyThe header that makes a retried payout safe.
- Beneficiaries API referenceEvery field and the five endpoints.