TagPayTagPay

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"
}
}