Skip to main content

Bank Transfers

Bank transfers send money from a PayKore wallet to an external Nigerian bank account, via the NIP (NIBSS Instant Payment) banking rails through PayKore's PSP partners.

Unlike P2P transfers, bank transfers are asynchronous. You receive a 202 Accepted immediately, then a webhook when the transfer confirms — typically seconds to minutes later.

Fee: ₦50 flat (5000 kobo) per transfer.


Verify the bank account first

Before initiating a transfer, verify the account number resolves to a real account. This prevents sending money to a mistyped account number — funds sent to the wrong NUBAN are very difficult to recover.

curl "https://api.paykore.dev/v1/banks/resolve?bank_code=058&account_number=0123456789" \
-H "X-API-Key: sk_test_YOUR_KEY_HERE"

Response:

{
"account_number": "0123456789",
"account_name": "ADEBAYO OLUWASEUN JAMES",
"bank_code": "058",
"bank_name": "Guaranty Trust Bank",
"meta": {
"request_id": "req_3pQrS4tUvW"
}
}

Display account_name to your user and ask them to confirm it matches the intended recipient before submitting the transfer.

Invalid account number:

{
"error": {
"code": "ACCOUNT_RESOLUTION_FAILED",
"message": "Could not resolve this account number with the specified bank.",
"http_status": 422
},
"meta": {
"request_id": "req_8rStU9vWxY"
}
}
warning

Always resolve the account and show the account name for user confirmation before submitting a bank transfer. PayKore cannot reverse a transfer sent to a valid but unintended account.


Initiating the transfer

curl -X POST https://api.paykore.dev/v1/transfers/bank \
-H "X-API-Key: sk_test_YOUR_KEY_HERE" \
-H "Idempotency-Key: payout_20260602_001" \
-H "Content-Type: application/json" \
-d '{
"source_wallet_id": "wlt_9f3kA2mXpQ",
"dest_account": {
"bank_code": "058",
"account_number": "0123456789",
"account_name": "ADEBAYO OLUWASEUN JAMES"
},
"amount_kobo": 1000000,
"reference": "payout_456",
"description": "Withdrawal to bank"
}'

Response — 202 Accepted:

{
"id": "tx_8wXyZ1aBcD",
"type": "bank_transfer",
"status": "processing",
"reference": "payout_456",
"amount_kobo": 1000000,
"amount_naira": "10000.00",
"fee_breakdown": {
"customer_fee_kobo": 5000,
"platform_fee_kobo": 1000,
"mfb_cost_kobo": 4000
},
"dest_account": {
"bank_code": "058",
"bank_name": "Guaranty Trust Bank",
"account_number": "0123456789",
"account_name": "ADEBAYO OLUWASEUN JAMES"
},
"created_at": "2025-06-01T11:00:00Z",
"meta": {
"request_id": "req_4tUvW5xYzA"
}
}

Key point: the source wallet is debited immediately upon acceptance (you can confirm via source_wallet.balance_after_kobo if included, or a follow-up GET). The actual bank payout is in flight — status: "processing" means PayKore has accepted the request and is routing it through the PSP. It is not yet confirmed by the receiving bank.


The async lifecycle

Typical timing:

StageDuration
202 response to your serverUnder 1 second
PSP payout during banking hours10–30 seconds typically
PSP payout outside banking hours / weekends / public holidaysUp to 24 hours

Design your UI to reflect "processing" rather than assuming completion at the 202 response. Show a pending state to your user until the webhook confirms.


Handling the webhook

On success — transaction.completed:

{
"event": "transaction.completed",
"data": {
"id": "tx_8wXyZ1aBcD",
"type": "bank_transfer",
"status": "completed",
"reference": "payout_456",
"amount_kobo": 1000000,
"psp_reference": "FLW-PO-9928374651",
"completed_at": "2025-06-01T11:00:24Z"
},
"timestamp": "2025-06-01T11:00:25Z",
"webhook_id": "wh_2cDeF3gHiJ"
}

On failure — transaction.failed:

{
"event": "transaction.failed",
"data": {
"id": "tx_8wXyZ1aBcD",
"type": "bank_transfer",
"status": "failed",
"reference": "payout_456",
"amount_kobo": 1000000,
"failure_reason": "Beneficiary bank rejected: account dormant",
"reversal": {
"reversed": true,
"reversal_tx_id": "tx_9xYzA2bCdE",
"amount_kobo": 1000000
}
},
"timestamp": "2025-06-01T11:01:10Z",
"webhook_id": "wh_5fGhI6jKlM"
}
note

On failure, the source wallet is automatically credited back the full debited amount (including fees) via the reversal object. You don't need to issue a manual refund — just update your records to reflect the failed state.

Reconciliation pattern:

async function handleWebhook(event: WebhookEvent) {
const { id: transactionId, status, reference } = event.data;

const payout = await db.payouts.findByReference(reference);
if (!payout) {
logger.warn('Webhook for unknown reference', { reference });
return;
}

await db.payouts.update(payout.id, {
status,
paykoreTransactionId: transactionId,
completedAt: status === 'completed' ? new Date() : null,
});
}

Polling as a fallback

If your webhook endpoint was temporarily down, you can poll for the current status using your reference:

curl "https://api.paykore.dev/v1/wallets/wlt_9f3kA2mXpQ/transactions?reference=payout_456" \
-H "X-API-Key: sk_test_YOUR_KEY_HERE"
tip

Implement webhooks as your primary mechanism for status updates. Use polling only as a fallback — for example, a scheduled job that checks any processing transfers older than 5 minutes, in case a webhook delivery was missed.


Bank codes reference

BankCode
Guaranty Trust Bank (GTBank)058
Access Bank044
Zenith Bank057
First Bank of Nigeria011
United Bank for Africa (UBA)033
First City Monument Bank (FCMB)214
Sterling Bank232
Wema Bank035
Fidelity Bank070
Union Bank032

This is a partial list of major banks. The full, up-to-date list is available via:

curl https://api.paykore.dev/v1/banks \
-H "X-API-Key: sk_test_YOUR_KEY_HERE"

See the API Reference for the complete endpoint schema.


Sandbox behaviour

In sandbox, bank transfers complete immediately — within 1–2 seconds — using a fake PSP reference. The transaction.completed webhook fires automatically with no real money moving. This lets you build and fully test your webhook handler without waiting on real banking rails.

note

Sandbox does not simulate failures by default. To test your transaction.failed handling path, use a destination account number ending in 0000 — this is a reserved sandbox pattern that always triggers a simulated failure with reversal.


Next steps