Pay With Bank Transfer
Receive Bank transfer payments for NGN transactions.
Pay with bank transfer (PWBT
) allows you to receive payments via bank transfers initiated by customers. Each payment is made into a virtual bank account linked to your settlement wallet.
You can generate either a dynamic or static virtual bank account to accept these payments:
- Dynamic accounts are generated per transaction and expire after use. Ideal for one-time payments.
- Static accounts are permanent and reusable, making them suitable for recurring payments.
PWBT is currently available for transactions in NGN (Nigerian Naira) and GHS (Ghanaian Cedi) only.
Pay With Bank (PWB) vs Pay With Bank Transfer (PWBT)
While both options involve a customer's bank account, the payment experience differs:
- Pay with Bank (PWB) debits the customer's bank account directly after online authentication.
- Pay with Bank Transfer (PWBT) requires the customer to manually initiate a transfer from their bank app.
Feature | Pay with Bank (PWB) | Pay with Bank Transfer (PWBT) |
---|---|---|
Transaction Type | Asynchronous (requires webhook or callback) | Asynchronous (requires webhook or callback) |
Authentication Type | Online banking login | Customer-initiated transfer (PIN input) |
In-app/Browser support | Yes (redirect to customer’s bank page) | No |
Requirements
Before integrating mobile money payments, complete the following steps:
- Retrieve your API keys from the dashboard.
- Notify your customers that they will need to complete the payment by initiating a transfer from their bank account.
How PWBT Works
Customers using Pay with Bank Transfer (PWBT) must complete their payment by transferring funds to a system-generated bank account.
The transaction is only successful once the customer initiates and completes the transfer to the provided virtual account.
Payment Flow
Follow these steps to accept payments via bank transfer:
-
Generate the virtual account details: Create a static or dynamic virtual bank account linked to the transaction.
-
Display the account details to the customer – Prompt the customer to transfer the specified amount to the generated account.
-
Listen for webhooks – Monitor the webhook events associated with the virtual account to detect successful transfers.
-
Verify the payment – Before fulfilling the order or service, confirm:
status
issuccessful
amount
matches the expected amountcustomer_id
is validid
(transaction reference) is consistent with the original request
How to Create Virtual Accounts
You can receive payments through either a static or dynamic virtual account. Although both types look and feel the same to customers, they operate quite differently.
Static Virtual accounts
Static virtual accounts (also called permanent accounts) do not expire. Once created, they can be assigned to a single customer and reused for multiple transactions.
Static Account Validity
Static accounts return an expiry date set to 100 years from their creation date.
To create a static virtual account, collect the following customer information:
- Customer name, preferably the first and last name.
- Customer email.
- The Customer's national identification number. For NGN virtual accounts, either of the following is required:
- Bank Verification Number (BVN)
- National Identification Number (NIN)
Use the Customer's personal Identification information (PII): the name and email to create a customer object. Store the customer_id
from the response for the next step.
curl --location 'https://api.flutterwave.cloud/developersandbox/customers' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'X-Idempotency-Key: {{YOUR_UNIQUE_INDEMPOTENCY_KEY}}' \
--header 'Authorization: Bearer {{YOUR_ACCESS_TOKEN}}' \
--data-raw '{
"name": {
"first": "Cornelius",
"last": "Ashley-Osuzoka"
},
"email": "[email protected]"
}'
You'll get a response similar to this:
{
"status": "success",
"message": "Customer created",
"data": {
"id": "cus_WWVaC0InrN",
"email": "[email protected]",
"name": {
"first": "Cornelius",
"last": "Ashley-Osuzoka"
},
"meta": {},
"created_datetime": "2025-06-02T07:40:48.637002170Z"
}
}
Use the customer_id
along with other required details to create the virtual account using the create virtual account endpoint.
curl --location 'https://api.flutterwave.cloud/developersandbox/virtual-accounts' \
--header 'Authorization: Bearer {{YOUR_ACCESS_TOKEN}}' \
--header 'X-Idempotency-Key: {{YOUR_UNIQUE_INDEMPOTENCY_KEY}}' \
--header 'Content-Type: application/json' \
--data '{
"reference": "1ca9e18f-f038-436f-b32b-3b7facdb1e13",
"customer_id": "cus_WWVaC0InrN",
"amount": 1500,
"currency": "NGN",
"account_type": "static",
"narration": "Cornelius Ashley-Osuzoka",
"bvn": "12345678901"
}'
On a successful request, you'll get a response similar to this:
{
"status": "success",
"message": "Virtual account created",
"data": {
"id": "van_b6v7ZkuLug",
"amount": 0,
"account_number": "3788163576",
"reference": "deac44b2-f8bd-4492-be07-bc25e5c4b159",
"account_bank_name": "WEMA BANK",
"account_type": "static",
"status": "active",
"account_expiration_datetime": "3024-10-03T07:46:47.519984192Z",
"note": "Please make a bank transfer to Cornelius Ashley-Osuzoka",
"customer_id": "cus_WWVaC0InrN",
"created_datetime": "2025-06-02T07:46:47.529511629Z",
"meta": {}
}
}
A failed response looks like this:
{
"status": "failed",
"error": {
"type": "REQUEST_NOT_VALID",
"code": "10400",
"message": "Request is not valid",
"validation_errors": [
{
"field_name": "bvn",
"message": "bvn must be exactly 11 characters long and a signed integer"
}
]
}
}
Dynamic Virtual Accounts
Dynamic virtual accounts are temporary and expire after a set period. They're intended for one-time use or time-bound payments.
Dynamic Account Usage
Do not allow customers to save dynamic account details. These details are non-reusable and expire after the specified duration.
You must define an expiry when creating a dynamic account.
- Maximum expiry: 365 days (or
31536000
seconds) - Default (if not specified): 1 hour (3600 seconds)
To create a dynamic account, you'll first need to set up the customer object.
curl --location 'https://api.flutterwave.cloud/developersandbox/customers' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'X-Idempotency-Key: {{YOUR_UNIQUE_INDEMPOTENCY_KEY}}' \
--header 'Authorization: Bearer {{YOUR_ACCESS_TOKEN}}' \
--data-raw '{
"name": {
"first": "Cornelius",
"last": "Ashley-Osuzoka"
},
"email": "[email protected]"
}'
You'll get a response similar to this:
{
"status": "success",
"message": "Customer created",
"data": {
"id": "cus_WWVaC0InrN",
"email": "[email protected]",
"name": {
"first": "Cornelius",
"last": "Ashley-Osuzoka"
},
"meta": {},
"created_datetime": "2025-06-02T07:40:48.637002170Z"
}
}
Send the customer_id
and other relevant information to the create virtual account endpoint. Be sure to set account_type
to dynamic
and specify the expiry
period.
curl --location 'https://api.flutterwave.cloud/developersandbox/virtual-accounts' \
--header 'Authorization: Bearer {{YOUR_ACCESS_TOKEN}}' \
--header 'X-Idempotency-Key: {{YOUR_UNIQUE_INDEMPOTENCY_KEY}}' \
--header 'Content-Type: application/json' \
--data '{
"reference": "a4d5f6b8-a785-4d41-8932-50fd8288aec8,
"customer_id": "cus_WWVaC0InrN",
"expiry": 60,
"amount": 1500,
"currency": "NGN",
"account_type": "dynamic",
"narration": "Cornelius Ashley-Osuzoka",
"bvn": "12345678901"
}'
On a successful request, you'll get a response similar to this:
{
"status": "success",
"message": "Virtual account created",
"data": {
"id": "van_fRiLt0WNsj",
"amount": 1500,
"account_number": "4032866864",
"reference": "9d961ebf-6e51-4970-a334-af6a39325930",
"account_bank_name": "WEMA BANK",
"account_type": "dynamic",
"status": "active",
"account_expiration_datetime": "2025-06-02T08:03:21.369640550Z",
"note": "Please make a bank transfer to Cornelius Ashley-Osuzoka",
"customer_id": "cus_WWVaC0InrN",
"created_datetime": "2025-06-02T08:02:21.383710209Z",
"meta": {}
}
}
A failed response looks like this:
{
"status": "failed",
"error": {
"type": "REQUEST_NOT_VALID",
"code": "10400",
"message": "Request is not valid",
"validation_errors": [
{
"field_name": "bvn",
"message": "bvn must be exactly 11 characters long and a signed integer"
}
]
}
}
Verifying PWBT Transactions
Webhook Requirement
Before proceeding, ensure you’ve read our webhook management guide for proper webhook setup and handling.
When a customer completes a transfer to a virtual account, Flutterwave sends a charge.completed
webhook to your configured webhook URL. This webhook contains details about the transaction and the associated customer.
{
"webhook_id": "wbk_xCBGoxP44NzL74hcCJiV",
"timestamp": 1748850422635,
"type": "charge.completed",
"data": {
"id": "chg_zH0BLoNltt",
"amount": 175,
"currency": "NGN",
"customer": {
"id": "cus_WWVaC0InrN",
"address": null,
"email": "[email protected]",
"name": {
"first": "Cornelius",
"middle": null,
"last": "Ashley-Osuzoka"
},
"phone": null,
"meta": {},
"created_datetime": "2025-06-02T07:40:48.637Z"
},
"description": null,
"meta": {},
"payment_method": {
"type": "bank_transfer",
"bank_transfer": {
"account_expires_in": null,
"account_display_name": null,
"account_type": null
},
"id": "pmd_NzbQZvbnPj",
"customer_id": null,
"meta": {},
"device_fingerprint": null,
"client_ip": null,
"created_datetime": "2025-06-02T07:46:47.520Z"
},
"redirect_url": null,
"reference": "deac44b2-f8bd-4492-be07-bc25e5c4b159",
"status": "succeeded",
"processor_response": {
"type": "approved",
"code": "00"
},
"created_datetime": "2025-06-02T07:47:02.537812148Z"
}
}
Use the data.id
from the webhook to verify the transaction.
curl --location 'https://api.flutterwave.cloud/developersandbox/charges/chg_HJRm0DeDIN' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {{YOUR_ACCESS_TOKEN}}'
You'll get a response similar to this:
{
"status": "success",
"message": "Charge fetched",
"data": {
"id": "chg_zH0BLoNltt",
"amount": 175,
"fees": [
{
"type": "vat",
"amount": 0
},
{
"type": "app",
"amount": 0
},
{
"type": "merchant",
"amount": 0
},
{
"type": "stamp_duty",
"amount": 0
}
],
"currency": "NGN",
"customer_id": "cus_WWVaC0InrN",
"settled": true,
"settlement_id": [
"stm_XPx038OwdI"
],
"meta": {},
"payment_method_details": {
"type": "bank_transfer",
"bank_transfer": {},
"id": "pmd_NzbQZvbnPj",
"meta": {},
"created_datetime": "2025-06-02T07:46:47.520Z"
},
"reference": "deac44b2-f8bd-4492-be07-bc25e5c4b159",
"status": "succeeded",
"processor_response": {
"type": "approved",
"code": "00"
},
"created_datetime": "2025-06-02T07:47:02.945Z"
}
}
Before confirming payment or fulfilling an order:
status
is succeeded.amount
matches the expected charge.currency
is correct.customer_id
matches the user.reference
is consistent with your internal tracking.
After verifying your transaction, you can track all the payments made into a virtual account by querying the charge list using the virtual account's ID.
curl --location 'https://api.flutterwave.cloud/developersandbox/charges?virtual_account_id=van_b6v7ZkuLug' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {{YOUR_ACCESS_TOKEN}}'
You'll get a response similar to this:
{
"status": "success",
"message": "Charges fetched",
"meta": {
"page_info": {
"total": 1,
"current_page": 1,
"total_pages": 1
}
},
"data": [
{
"id": "chg_zH0BLoNltt",
"amount": 175,
"fees": [
{
"type": "vat",
"amount": 0
},
{
"type": "app",
"amount": 0
},
{
"type": "merchant",
"amount": 0
},
{
"type": "stamp_duty",
"amount": 0
}
],
"currency": "NGN",
"customer_id": "cus_WWVaC0InrN",
"settled": true,
"settlement_id": [
"stm_XPx038OwdI"
],
"meta": {},
"payment_method_details": {
"type": "bank_transfer",
"bank_transfer": {},
"id": "pmd_NzbQZvbnPj",
"meta": {},
"created_datetime": "2025-06-02T07:46:47.520Z"
},
"reference": "deac44b2-f8bd-4492-be07-bc25e5c4b159",
"status": "succeeded",
"processor_response": {
"type": "approved",
"code": "00"
},
"created_datetime": "2025-06-02T07:47:02.945Z"
}
]
}
Testing your integration
Use the X-Scenario-Key
header to simulate transaction outcomes during testing.
Simulate a successful transaction by specifying issuer:approved
.
curl --location 'https://api.flutterwave.cloud/developersandbox/virtual-accounts' \
--header 'Authorization: Bearer {{YOUR_ACCESS_TOKEN}}' \
--header 'X-Scenario-Key: issuer:approved' \
--header 'X-Idempotency-Key: {{YOUR_UNIQUE_INDEMPOTENCY_KEY}}' \
--header 'Content-Type: application/json' \
--data '{
"reference": "cb0e10d5-f59e-424b-af44-65445ae4472b",
"customer_id": "cus_WWVaC0InrN",
"amount": 1500,
"currency": "NGN",
"account_type": "static",
"narration": "Cornelius Ashley-Osuzoka",
"bvn": "12345678901"
}'
A failed transfer using the issuer:failed
.
curl --location 'https://api-sit.flutterwave.cloud/developersandbox/virtual-accounts' \
--header 'Authorization: Bearer {{YOUR_ACCESS_TOKEN}}' \
--header 'X-Scenario-Key: issuer:failed' \
--header 'X-Idempotency-Key: {{YOUR_UNIQUE_INDEMPOTENCY_KEY}}' \
--header 'Content-Type: application/json' \
--data '{
"reference": "cb0e10d5-f59e-424b-af44-65445ae4472b",
"customer_id": "cus_WWVaC0InrN",
"amount": 1500,
"currency": "NGN",
"account_type": "static",
"narration": "Cornelius Ashley-Osuzoka",
"bvn": "12345678901"
}'
Next Steps
That’s it! You’ve now successfully integrated the bank transfer payment method. It doesn't end there, there is more:
- Learn about settlements of successful payments into your Flutterwave balance.
- For cases where refunds are necessary, see the refunds guide for more information on how to process transaction refunds.
Updated about 16 hours ago