Skip to content

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 and beneficiaries, accept a private key only.

RoutePublic keyPrivate key
/payments, /accountsAcceptedAccepted
/transfers, /balance, /refunds, /disputes, /beneficiaries406Accepted

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 reason on the transfer object. Three cover most of it.

reasonWhat happenedWhat to do
insufficient_balanceYour available balance did not cover amount plus feesTop up, then create a new transfer
invalid_recipientThe number or account is not valid for that channelFix the recipient, no funds moved
recipient_unregisteredNo wallet exists on that number for that operatorCheck the number against the operator prefix

The second and third are the same mistake most of the time: a number that belongs to another operator than the channel you named. A Cameroonian number starting +23767 is MTN and +23769 is Orange, and sending one to the other channel fails without costing you anything. Phone numbers has the prefix table.

insufficient_balance deserves a word on timing. 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, failed, or cancelled if the provider itself aborts it.

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

Payouts carry a ceiling of their own, on top of the four general ones: 20 per minute per team, flat, independent of your plan. A loop over a list of beneficiaries will meet it well before it meets the team quota.

The 429 for that ceiling arrives without the type field that the general ceilings set, so a handler that reads type blindly will fail on the very response it needs to read.

What a payout throttle answers
{
"code": 429,
"status": "Too Many Requests",
"message": "Too Many requests. Merchant limit : 360",
"retry_after": 24
}

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.

There is no bulk transfer

No CSV upload, no batch endpoint, no array of recipients. Every transfer is one POST /transfers with one beneficiary, which is what the 20 per minute ceiling is sized for.

If you are paying many people, the shape that works is a queue with a bounded worker pool, an idempotency key derived from the payout row, and a retry on the queue rather than on the call.

The recipient says they received nothing

Before suspecting the transfer, check what it reads.

Ask the transfer, not the recipient
curl https://api.wajub.com/transfers/po_9xh0XQ5wu2lxCSUGajfv \
  -H "Authorization: sk.kZ3qP8mWvL2xR7tB5nY4hC6dF9jS1aG0eU3i…" \
  | jq '.transfer | { status, 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.

What did you think of this content?