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

RoutePublic keyPrivate key
/paymentsAcceptedAccepted
/transfers, /balance, /refunds, /disputes, /beneficiaries, /accounts406Accepted

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_reasonWhat happenedWhat to do
invalid_recipientThe beneficiary is missing, or has no payout channelFix the beneficiary, then create a new transfer
no_providerNo provider is eligible for that channel and amount right nowRetry later, or pay on another channel
provider_rejectedEvery eligible provider refused the payoutRead 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.

Ask the transfer, not the recipient
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.

What did you think of this content?