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 insteadIf
Virtual Account TransfersYou 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/banks

Bank 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=1234567890

This 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/beneficiaries

Store 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-REFERENCE

Money 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:

  1. A payout succeeds and your ledger updates once.
  2. Name enquiry mismatch blocks the payout in your UI.
  3. Insufficient funds is handled without a retry storm.
  4. A duplicate request does not send twice.
  5. transfer.reversed unwinds a completed payout.

Item 4 is the one that costs real money if it is wrong.

Next


Did this page help you?