Transfers
Send money from your balance to a bank account.
A transfer moves money out of your available balance to a bank account — paying a supplier, a customer refund, a marketplace seller, or your own account.
Payouts are the direction where mistakes cost the most. Money that leaves is hard to recall, so this guide leans on the checks that happen before you send.
| Use instead | If |
|---|---|
| Virtual Account Transfers | You are paying out from the virtual-account rail, or sending in bulk. |
The Flow
sequenceDiagram
participant Y as Your backend
participant K as Kyshi
participant B as Beneficiary bank
Y->>K: GET /v1/transfer/banks
Y->>K: GET /v1/transfer/name-enquiry
K-->>Y: Account holder name
Note over Y: Confirm it matches
Y->>K: POST /v1/transfer/beneficiaries
K-->>Y: beneficiaryId
Y->>K: POST /v1/transfer
K-->>Y: Accepted (pending)
K->>B: Payout
K-->>Y: transfer.success / failed / reversed
The two steps before sending — bank list, then name enquiry — are what stop money going to the wrong person.
Build It
1. Get the bank list
GET {{host}}/v1/transfer/banksBank codes are not stable across providers or markets. Fetch the list rather than hardcoding it, and let the customer pick from it.
2. Resolve the account name
GET {{host}}/v1/transfer/name-enquiry?bankCode=000013&accountNumber=1234567890This returns the name registered against the account. Show it to whoever is authorising the payout and make them confirm it.
Bank transfers in these markets settle on account number alone — the name is not checked by the rail. A single wrong digit sends real money to a real stranger, and you will not get it back. Name enquiry is the only practical protection, and it costs one request.
3. Create a beneficiary
POST {{host}}/v1/transfer/beneficiariesStore the returned beneficiaryId. For anyone you will pay more than once, this removes the chance of re-typing the account number wrongly later.
For genuine one-offs you can send an inline beneficiary instead of saving one.
4. Initiate the transfer
-H "x-api-key: your_secret_key"POST {{host}}/v1/transfer{
"beneficiaryId": "bnf_abc123",
"amount": 50000,
"currency": "NGN",
"narration": "Payout for ORDER-10001"
}narration appears on the beneficiary's statement. Put something they will recognise.
5. Wait for the outcome
A successful response means accepted, not delivered. Act on transfer.success, transfer.failed, or transfer.reversed. See Webhook Events.
Balance And Fees
Kyshi checks that your available balance covers the amount plus the payout fees before sending. If it does not, the transfer is rejected with an insufficient funds error.
Attempting to pay out your exact available balance will therefore usually fail — the fee has nowhere to come from. Leave headroom.
Note also that available is not the same as total: funds still pending settlement cannot be paid out, and in-flight payouts are already reserved against your balance. See Balances.
When It Fails
Insufficient funds
Either you genuinely lack the money, or you forgot the fee, or the funds are pending settlement rather than available. Check available balance specifically, not a total.
The payout fails at the bank
transfer.failed. Funds are returned to your balance. Usual causes are a closed or frozen account, or a bank outage. Re-running name enquiry will often reveal the account is no longer valid.
The payout reverses after success
transfer.reversed can arrive well after transfer.success. If your system marked the payout final and updated a seller's ledger, it must be able to unwind that.
You send twice
The most expensive failure mode. A network timeout on the request does not mean the payout did not happen — the response may simply have been lost.
Never retry a payout blindly. Fetch by your own reference first and check whether it already exists:
GET {{host}}/v1/transfer?query=YOUR-REFERENCEMoney goes to the wrong account
Recovery depends on the receiving bank's goodwill. This is why step 2 is not optional.
Reconcile
async function reconcilePendingPayouts() {
for (const payout of await db.payouts.pendingLongerThan({ minutes: 15 })) {
const remote = await kyshi.transfers.retrieve(payout.transferId);
switch (remote.status) {
case 'SUCCESS': await markPaid(payout); break;
case 'FAILED': await returnToLedger(payout); break;
case 'REVERSED': await unwind(payout); break;
// still pending: leave it
}
}
}Reconcile payouts more aggressively than collections. An unresolved payout is money in an unknown state.
Test It
Before going live, confirm:
- A payout succeeds and your ledger updates once.
- Name enquiry mismatch blocks the payout in your UI.
- Insufficient funds is handled without a retry storm.
- A duplicate request does not send twice.
transfer.reversedunwinds a completed payout.
Item 4 is the one that costs real money if it is wrong.
Next
- Banks · Name Enquiry — the pre-flight checks
- Initiate Transfer — full schema
- Balances — what you can actually send
Updated 5 days ago
