Guides
Bank transfers
Show the customer a one-time NUBAN account number to transfer to. Funds land via NIBSS within seconds, and TagPay fires a webhook once the amount matches the charge.
How it works
TagPay provisions a one-time NUBAN tied to a specific charge. The customer transfers from their bank app or a teller. If the amount matches charge.amount before the account expires, the charge succeeds.
The virtual account is single-use — it only accepts a transfer for the exact charge amount, and it stops accepting deposits the moment the charge resolves or the expiry window closes. Transfers for a different amount are rejected by the bank rail before they reach TagPay, so partial or over-payment is structurally impossible.
Provisioning
Call POST /v1/charge/bank_transfer to mint a one-time virtual account for a charge. The response includes a virtual_account object with the account number, bank name, and account name to display. (Hosted-page flows that pass payment_methods: ["bank_transfer"] on create render the same account number on TagPay's checkout page instead.)
If you need a virtual account outside a charge or checkout session, call POST /v1/collection-accounts with type: "expiring" and an expectedAmount in kobo.
{"status": true,"data": {"reference": "tp_charge_8mFp2Lq9","amount": 500000,"channel": "bank_transfer","virtual_account": {"account_number": "9012345678","bank_name": "Wema Bank","account_name": "TAGPAY / ACME LTD"},"expires_at": "2026-05-15T15:02:11.402Z"}}
Expiry
Expiring virtual accounts are valid for 15 minutes. The expiry is controlled by TagPay and cannot be extended from the API request. For checkout sessions, the hosted-page session has its own separate expires_in_minutes window, but the underlying expiring collection account remains a 15-minute exact-amount account.
Webhooks
Unlike card payments, bank-transfer success arrives asynchronously. The customer leaves your checkout page to pay from their bank app — there is no redirect back when the transfer lands.
Listen for the charge.success webhook and treat it as the source of truth. The verify endpoint is a polling backstop — useful for reconciliation, not the primary signal.
Permanent vs expiring
For most payment flows you want the expiring per-checkout account documented above — it guarantees a single, attributable charge. The permanent collection account is a separate primitive for open-ended deposits; create it with POST /v1/collection-accounts using type: "permanent" in live mode.
A standalone inflow to either account type — no checkout session involved — carries a reference prefixed COLL_ or SUBWALLET_, with no sessionId in the payload — a checkout-linked bank-transfer charge additionally carries sessionId and sessionKey. Either way, a static account only ever fires charge.success — there's no charge.failed or charge.reversed for a virtual account that simply never receives a payment.
{"event": "charge.success","data": {"id": "6f1e0f2c-9b7a-4c3d-8e21-4a5b6c7d8e9f","reference": "COLL_e3a6c8b1-2f9d-4b7e-8a1c-6d5f3b9e2c74_TXN8837462910","gatewayReference": "GW_4a1f7c2b5e3d6f70","amount": 500000,"currency": "NGN","fee": 17500,"netAmount": 482500,"status": "success","paymentMethod": "bank_transfer","cardBrand": null,"cardLast4": null,"bankName": "Zenith Bank","accountNumber": "0142897654","senderName": "Ifeoma Chukwu","senderBankName": "Zenith Bank","senderAccountNumber": "0142897654","destinationBankName": "TagPay","destinationAccountNumber": "9012345678","destinationAccountName": "TAGPAY / ACME LTD","collectionAccount": {"id": "e3a6c8b1-2f9d-4b7e-8a1c-6d5f3b9e2c74","reference": "acme8f2a_PRIMARY","accountNumber": "9012345678","accountName": "TAGPAY / ACME LTD"},"paidAt": "2026-07-20T09:14:32.000Z","processedAt": "2026-07-20T09:14:32.000Z","createdAt": "2026-07-20T09:14:10.000Z"}}