Transfers & Payouts
Send money out, what it leaves from, and what has to be true first.
Everything else in these docs is money coming in. This section is money going the other way: to a supplier, a driver, a seller on your marketplace, or to your own account.
A payout is not the mirror image of a payment. A payment either succeeds or does not cost you anything, while a payout spends funds you already hold, which is why it passes through more checks than anything else on the API.
1Only available can leave
Pending is collected but not settled, reserved is already committed. A payout draws on neither.
2Twelve checks come first
Each answers with its own 422. Nothing is reserved until all of them pass, so a refusal leaves nothing behind.
3The fee is held too
Sending 10 000 XAF reserves 10 100. The 1 % is charged on completion, but it is held from the start.
What ships today
| What you need | Where |
|---|---|
| Pay one person or business | Transfers |
| Keep a list of people you pay often | Beneficiaries |
| Know what is available to send, and what has already been settled | Balance & settlements |
| Pay many people at once | POST /transfers/batch, up to 500 lines per call |
| Pay the same person on a schedule | Settings, Transfers, Automation, from the Growth plan |
A batch takes up to 500 payouts in one call, on every plan. Its Idempotency-Key header is
required, and each line carries a reference unique within the batch. Every line passes the same
checks as a single payout, the currency's bounds included, and names its beneficiary by id only: a
phone number or an e-mail is refused with beneficiary_not_found. Each line succeeds or fails on its
own, so one seller without a verified destination does not stop the others. Sending the same batch
again with the same key only sends the lines that did not go through. The Dashboard has no CSV
import for payouts.
Two things refuse a batch whole, before any line runs. A malformed line, such as a missing field, a
currency that is not three letters or a reference repeated in the batch, answers 422 for the
whole call, with the faulty line named in errors (transfers.3.amount). The other is a missing
or empty Idempotency-Key header. Keep that key to letters, digits and . _ : -.
For the hourly velocity ceiling, a batch is one payout call whatever its number of lines, and each line adds its amount to the hourly amount.
A recurring transfer pays one beneficiary a fixed amount every hour, day, week or month. Each run creates an ordinary payout, through every check below, and a paused schedule can be resumed.
Where the money leaves from
Wajub holds a balance per currency for your account. Collections land in it, payouts draw from it, and nothing reaches a recipient that the balance did not cover first.
https://api.wajub.com/balanceOne call answers for the environment your key belongs to, and splits the money into five figures that do not mean the same thing.
available is the only figure a payout can draw on. pending is money collected but not yet
settled to you, and reserved is money already committed to payouts in flight. Pass ?currency=
to read the same balance converted into another currency.
The balance can lag by up to three minutes
The figure can lag by up to three minutes, so a payout you created a minute ago may not have moved
available yet. Do not use it as a running total inside a loop: check it once before a batch,
then trust the transfer calls themselves, which fail on insufficient funds rather than
overdrawing. In sandbox the response is flatter, total and available carry the same figure and
pending and reserved stay at zero.
What a payout costs
On the Wajub rail, a payout is charged a flat 1 % by default, and your plan can set a different
rate per channel. A payout to another Wajub account (wajub) is free, and a bank transfer (wire)
carries a fixed fee instead of a rate, see Fees & pricing. The fee is
charged when the transfer completes, but it is held with the principal at creation: sending
10 000 XAF reserves 10 100 XAF, not 10 000.
Routing to your own provider account changes that. A payout that leaves through a provider you brought yourself is not funded from your Wajub balance, so it carries no Wajub fee and reserves nothing. The provider bills you directly on its own terms.
What has to be true before a payout executes
This is the part worth reading before your first live transfer. Creating one runs through a series
of checks, each of which answers 422 with its own message and its own error_code rather than a
generic failure. Branch on the code; the full list is in Errors.
| Check | What it looks at | error_code |
|---|---|---|
| Amount bounds | The minimum and maximum configured for that currency | amount_out_of_range |
| The beneficiary | Resolvable on your account, active, and carrying a payment method | beneficiary_not_found, beneficiary_inactive, beneficiary_missing_payment_method |
| The channel | A payout channel: Mobile Money, a Djamo wallet or another Wajub account through the API, and a bank account through a payout account | channel_not_supported |
| Payouts paused platform-wide | Payouts suspended across Wajub, for everyone | payouts_suspended |
| Account restrictions | An active transfers restriction on your account | merchant_restricted |
| Negative balance | No payout leaves while available is below zero, from any currency | negative_balance |
| Sync Lite reserve | A Lite merchant's payout cannot dip into the slice held against refunds | reserve_held |
| Destination verification | The payment method has been verified | beneficiary_not_verified |
| Destination cooling-off | A destination added or changed moments ago cannot be paid until its payout_allowed_after has passed | beneficiary_cooling_off |
| Your own daily cap | The count and the amount you set in Transfer security settings. Today's payouts in every currency are converted to XAF before the amount is compared. They count whether pending, held in review, processing or succeeded; failed and cancelled ones do not | daily_count_limit_exceeded, daily_amount_limit_exceeded |
| The platform ceiling | A hard limit by KYC tier, which you cannot raise yourself | single_payout_limit_exceeded, daily_payout_limit_exceeded, monthly_payout_limit_exceeded, payouts_not_enabled |
| Velocity | The hourly ceilings on accepted payout calls and on the amount paid out | too_many_payouts, hourly_payout_amount_exceeded |
| Channel ceiling | The per-transfer limit of that operator | amount_above_channel_limit |
| Provider availability | A payout route exists for that channel and currency | no_provider |
| Funds | available covers the amount plus the fee, on a Wajub route | insufficient_balance |
The order matters less than the principle: nothing is reserved until every check has passed, and a
transfer that fails one of them never existed. One thing does outlive the refusal: a beneficiary
you passed inline, as an object, is created before the checks run. It stays on your account and in
GET /beneficiaries, without a webhook. A retry with the same channel and number pays that same
beneficiary, under the name it was first created with, rather than creating a second one.
Your first live payout does not execute immediately
A payout is held for review, rather than sent, when any of these is true: it is one of your
account's first live payouts, its amount is above a platform review threshold, its amount crosses
a threshold you set yourself, your account asked for manual confirmation of its own payouts, or
the amount is unusual for your account. A held transfer is created with status review, its funds
are reserved, and it waits for Wajub to resolve it. Plan the first one, and do not run a launch
through an unattended loop.
The two thresholds that are yours live under Settings, Transfers: manual confirmation, with an optional amount below which it does not apply, on the General tab, and the review amount and the daily count and amount caps on the Security tab. A hold they trigger is still resolved by Wajub, not from your own Dashboard, so a session that was taken over cannot clear it.
The same Security tab carries two switches that apply to payouts started from the Dashboard, on
every plan. To protect API payouts, use restricted keys and an API IP
allow-list. Require 2FA refuses a
payout unless the member confirmed their password and their second factor in the last 30 minutes.
IP whitelist refuses every payout as long as the team has no Dashboard IP list: the list itself
lives in team security, and this
switch only makes sure it exists. Both settings are edited in live, by the owner or a member with
manage_settings.
Creating one
https://api.wajub.com/transfersA transfer takes an amount, a currency, and a recipient. The recipient is either a beneficiary you registered earlier, passed as its id, or the details themselves, in which case the beneficiary is created as part of the call.
curl https://api.wajub.com/transfers \
-H "Authorization: sk.xxxxxxxxxxxxxxxx…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: payout-892" \
-d '{
"beneficiary": "ben_7Kq2mX9vL4tRb3nP8sZc",
"amount": 10000,
"currency": "XAF",
"description": "Supplier payment #892"
}'The answer is 201 Created and the transfer comes back as processing, already on its way. Both
environments behave the same way here: a sandbox transfer draws on your sandbox balance and settles
itself a couple of seconds later, with the outcome decided by the recipient's test number.
There is no cancel
The API creates, lists and retrieves transfers. It does not cancel them. Once a transfer is
created it runs to succeeded or failed, and the only thing that pauses one is a review hold,
which you do not control. Treat POST /transfers as irreversible and put your checks before it,
not after.
The resources
| Method | Endpoint | For |
|---|---|---|
POST GET | /transfers | Create, list and retrieve a payout |
POST GET PUT DELETE | /beneficiaries | Keep the people you pay |
GET | /balance | What is available to send |
POST | /transfers/batch | Pay up to 500 people in one call |
POST | /beneficiaries/{id}/verify | Check a wallet belongs to the name you expect |
/identity/resolve answers 501 in live: it only works in sandbox, so it cannot vouch for a real
recipient. The name check that counts is POST /beneficiaries/{id}/verify: it asks the operator in
live mode and returns the name it holds, for information.
Checking the name on a wallet
All of them need the secret key. A public key is refused, and so is a browser: payout routes are
the most tightly held on the API. If you are paying out on behalf of a connected account, the
X-Sync header selects it, which Sync covers.
Related pages
- TransfersThe payout itself: statuses, routing, and what a failure means.
- Transfers QuickstartSend your first payout in sandbox, then live.
- BeneficiariesRegister a recipient once and pay them by id.
- Balance & settlementsWhat is available, what is pending, and what the fees were.
- IdempotencyThe header that makes a retried payout safe.