Transfers
Refused before sending, failed on the operator side, or throttled in a payout loop.
A transfer moves your money out, so it is guarded more tightly than a payment on every axis: the key that may call it, the rate at which you may call it, and the balance behind it. Most transfer problems are one of those three guards doing its job.
406 Private Key Required
You sent a public key. Payouts, along with balance, refunds, disputes, beneficiaries and Sync connections, accept a private key only.
| Route | Public key | Private key |
|---|---|---|
/payments | Accepted | Accepted |
/transfers, /balance, /refunds, /disputes, /beneficiaries, /accounts | 406 | Accepted |
Note the code. It is 406, not 403, so a branch written against 403 to catch a wrong key never
fires here.
403 This API key does not have permission to write transfer
You sent a restricted key without the scope. Payouts need transfer.write, and the read routes need
transfer.read. Scopes freeze when the key is created, so widening one means creating a new key.
The transfer failed immediately
Read failure_reason on the transfer object, not reason: reason is the label you supplied.
Three codes cover most of it.
failure_reason | What happened | What to do |
|---|---|---|
invalid_recipient | The beneficiary is missing, or has no payout channel | Fix the beneficiary, then create a new transfer |
no_provider | No provider is eligible for that channel and amount right now | Retry later, or pay on another channel |
provider_rejected | Every eligible provider refused the payout | Read failure_message, fix, then create a new one |
When a provider refuses, check first that the number belongs to the operator of the channel you
named. A Cameroonian number starting +23767 is MTN and +23769 is Orange.
Phone numbers has the prefix table, and
Failure reasons the other codes.
A balance too low for the payout is not a failed transfer: POST /transfers answers 422 with
error_code: insufficient_balance and nothing is created. Compare error_code, or read errors, never the
whole message. Timing is usually the cause. A collection that reads succeeded is not yet spendable: it becomes available after
settlement. Your balance has an available figure and a pending figure, and a payout draws on
available only.
I cannot cancel a transfer
There is no cancel endpoint. POST /transfers creates it, GET /transfers and
GET /transfers/{id} read it, and that is the whole surface. Once created, a transfer runs to
succeeded or failed, or cancelled when it is cancelled before it was sent. A payout the operator cancels on its side ends
failed.
This is deliberate: a payout executes with no human in the loop, and a cancel window would be a window for taking money back after the recipient has been credited. Validate before you send.
429 in a payout loop
Payout writes carry a ceiling of their own, on top of your plan's ceilings: 20 a minute per account
and environment, on every plan, Enterprise included, shared by payouts, batches, Sync payouts and
beneficiary creation. A loop over a list of beneficiaries will meet it well before it meets your
plan's ceiling. That ceiling answers 429 with type: payout_writes and retry_after. A live
payout can also answer 429 with error_code: payout_queue_busy and Retry-After: nothing was
created. Retry in a moment.
Space the loop, honour retry_after, and keep batch payouts away from any hour where a person might
be waiting on one. Rate limiting covers the pattern.
Paying many people at once
POST /transfers/batch sends up to 500 payouts in one call. The
payout brakes can refuse a batch whole with 422;
otherwise each line fails on its own: the response lists every line with its status, and a
rejected line carries a message.
After a timeout or a partial failure, send the whole batch again with the same Idempotency-Key:
only the lines that did not go through leave, the others come back as they already are. Never resend under a new key a batch you are not sure
failed: every line would be paid again. A line whose reference was already used for a different
payout is rejected rather than sent. The Dashboard has
no CSV upload.
The recipient says they received nothing
Before suspecting the transfer, check what it reads.
curl https://api.wajub.com/transfers/po_9xh0XQ5wu2lxCSUGajfv \
-H "Authorization: sk.xxxxxxxxxxxxxxxx…" \
| jq '.transfer | { status, failure_reason, amount, currency, created_at }'succeeded means the operator confirmed the credit. At that point the money is on the recipient's
side and the question belongs to them or to their operator, not to Wajub. processing means it is
still in flight, which on Mobile Money is usually under two minutes but can stretch when a network
is congested.