Transfers And Virtual Accounts

Collect by bank transfer and send payouts on both rails.

Three namespaces: one for collecting into virtual accounts, two for paying out.

kyshi.virtualAccounts

MethodEndpoint
create(params, options?)POST /v1/wallets
createCorporate(params, options?)POST /v1/wallets/corporate
list(params?, options?)GET /v1/wallets
retrieve(accountNumberOrAccountId, options?)GET /v1/wallets/{id}
getBalance(currency?, options?)GET /v1/wallets/balance
getDetails(businessId, options?)Wallet detail lookup
verifyPayment(params, options?)GET /v1/wallets/verify
simulateCredit(params, options?)POST /v1/wallets/credit
const account = await kyshi.virtualAccounts.create({
  customer: { email: '[email protected]', firstName: 'Ada', lastName: 'Okafor' },
  currency: 'NGN',
  accountCategory: 'VIRTUAL_ACCOUNT',
  amount: 5000,
  reference: 'ORDER-10001',
  feeBearer: 'CUSTOMER',
  expiresAt: '60',
});

// Show payableAmount, never requestedAmount.
display(account.accountNumber, account.bankName, account.payableAmount);

Showing requestedAmount instead of payableAmount is the most common cause of PARTIAL collections. See Virtual Accounts.

const result = await kyshi.virtualAccounts.verifyPayment({
  reference: 'ORDER-10001',
});

switch (result.collectionStatus) {
  case 'COMPLETED': return fulfilOnce('ORDER-10001');
  case 'OVERPAID':  return fulfilThenRefundExcess('ORDER-10001');
  case 'PARTIAL':   return notifyShortfall('ORDER-10001');
}

getBalance takes the currency positionally, not as an object:

const balance = await kyshi.virtualAccounts.getBalance('NGN');

In test mode, stand in for the customer:

await kyshi.virtualAccounts.simulateCredit({
  accountNumber: account.accountNumber,
  amount: '5070',
});

kyshi.transfers

MethodEndpoint
listBanks(currency?, options?)GET /v1/transfer/banks
nameEnquiry(params, options?)GET /v1/transfer/name-enquiry
createBeneficiary(params, options?)POST /v1/transfer/beneficiaries
listBeneficiaries(params?, options?)GET /v1/transfer/beneficiaries
retrieveBeneficiary(id, options?)GET /v1/transfer/beneficiaries/{id}
create(params, options?)POST /v1/transfer
list(params?, options?)GET /v1/transfer
retrieve(id, options?)GET /v1/transfer/{id}
const banks = await kyshi.transfers.listBanks('NGN');

const resolved = await kyshi.transfers.nameEnquiry({
  bankCode: '000013',
  accountNumber: '1234567890',
});

// Show resolved.accountName and have someone confirm it before sending.
const payout = await kyshi.transfers.create(
  { beneficiaryId: 'bnf_abc123', amount: 50000, currency: 'NGN', narration: 'Payout ORDER-10001' },
  { idempotencyKey: 'PAYOUT-10001' },
);

Always pass an idempotencyKey on payouts. Without one the SDK will not retry, and a lost response leaves you unsure whether the money moved. See Configuration.

kyshi.virtualAccountTransfers

Same methods as transfers, on the /v1/va/transfer rail, plus bulk:

MethodEndpoint
createBulk(params, options?)POST /v1/va/transfer/bulk
retrieveBulk(id, options?)GET /v1/va/transfer/bulk/{id}
const batch = await kyshi.virtualAccountTransfers.createBulk(
  { beneficiaryId: 'bnf_abc123', amount: 100_000_000, currency: 'NGN', narration: 'Supplier payout' },
  { idempotencyKey: 'BULK-10001' },
);

Bulk is one payout to one beneficiary, split because the rail caps at 5,000,000 NGN. It is not a way to pay many recipients. A batch can complete partially, so reconcile its children individually. See Virtual Account Transfers.

Beneficiary IDs are not shared between the two rails.

Next


Did this page help you?