Dynamic Virtual Accounts (Bank Transfer)
A dynamic virtual account is a one-time bank account number generated for a single transaction. Your customer pays the exact amount into it by bank transfer, and Echezona notifies you once the payment lands. Each account is tied to one transactionId and expires 1 hour after it is generated.
This guide walks through the full flow via the API: generating the account, displaying it to your customer, and confirming the payment.
Generating virtual accounts directly via the API is part of the Core API Integration flow, which requires the relevant licenses and compliance certifications before you can go live. If you'd rather not build your own checkout UI, use Checkout Integration instead — bank transfer is available on the hosted page out of the box. To begin the approval process, contact hello@echezona.com.
How It Works
- Initialize the transaction with your API key and get an
accessCode. - Get Payment Info with the
accessCodeto get thetokenthat authenticates the next call. - Make Payment with
paymentType: 5(Bank Transfer) to generate the virtual account. - Display the account details to your customer with a 1-hour countdown.
- Confirm the payment by receiving the webhook and verifying the transaction server-side.
All endpoints are relative to the base URL https://api.echezona.com/api. See Environments & Base URL.
Step 1: Initialize the Transaction
Create the transaction and receive an accessCode. See Initialize a Transaction for the full field reference.
Endpoint
POST Payments/Initialize
Headers
Authorization: Bearer
To authorize this API request, you need to include your apiKey as a bearer token. This is done by setting the Authorization header in your request. The header should have the format:
Authorization: "Bearer your_api_key"
Note: Get your API key from Settings > API Keys in your dashboard.
Request Body
{
"isLive": true,
"amount": "5000",
"currency": "NGN",
"email": "customer@example.com",
"phone": "08012345678",
"firstName": "John",
"lastName": "Doe",
"transactionId": "TXN123456789",
"callbackUrl": "https://example.com/payment/callback",
"metadata": [
{ "name": "OrderId", "value": "ORD987654321" }
]
}
The transactionId must be unique for every attempt.
Response Data
{
"responseCode": "00",
"responseMessage": "Authorization URL created",
"data": {
"paymentUrl": "https://checkout.echezona.com/2a11mganDV4u",
"accessCode": "2a11mganDV4u"
}
}
Keep the accessCode for the next step.
Step 2: Get Payment Info
Fetch the payment configuration for the transaction, including the token used to authenticate MakePayment. See Get Payment Details for the full response reference.
Endpoint
GET Payments/GetPaymentInfo/{{ accessCode }}
Request Params
accessCode(string) - The access code returned in Step 1.
Response Data
The fields you need from the response:
| Field | Type | Description |
|---|---|---|
token | string | Authenticates the MakePayment call in the next step (instead of your API key). |
transferPayment | boolean | Must be true — confirms bank transfer is enabled for your business. |
totalAmount_Transfer | number | The total the customer should transfer, including any fees. See Fees & Amounts. |
{
"responseCode": "00",
"responseMessage": "Payment information fetched successfully",
"data": {
"transactionId": "TXN123456789",
"transferPayment": true,
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"currency": "NGN",
"totalAmount_Transfer": 5000.00
}
}
Step 3: Generate the Virtual Account (Make Payment)
Call MakePayment with paymentType set to 5 (Bank Transfer) to generate the virtual account. See Make Payment for the full field reference.
Endpoint
POST Payments/MakePayment
Headers
Authorization: Bearer
To authorize this API request, you need to include your token as a bearer token. This is done by setting the Authorization header in your request. The header should have the format:
Authorization: "Bearer token"
The token property is gotten from the response data when you fetch
payment
meta
Request Body
{
"paymentType": 5,
"callbackUrl": "",
"country": "Nigeria",
"currency": "NGN",
"cvv": "",
"email": "customer@example.com",
"expiryMonth": "",
"expiryYear": "",
"firstname": "John",
"lastname": "Doe",
"metadata": "",
"phonenumber": "",
"productDescription": "",
"productId": "",
"transactionId": "TXN123456789",
"cardNumber": "",
"amount": "5000",
"accountName": "",
"accountNumber": "",
"narration": "Order ORD987654321",
"pin": "",
"bvn": "",
"deviceInformation": {
"httpBrowserColorDepth": 24,
"httpBrowserJavaEnabled": false,
"httpBrowserJavaScriptEnabled": true,
"httpBrowserLanguage": "en-US",
"httpBrowserScreenHeight": 800,
"httpBrowserScreenWidth": 1280,
"userAgentBrowserValue": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36",
"httpBrowserTimeDifference": "-60"
}
}
paymentTypeis5for Bank Transfer. See Code Mappings.transactionIdmust match the one you sent on Initialize.- Leave out
bankCode. When it isn't provided, Echezona routes the transfer to the optimal processor. - Card-only fields (
cardNumber,cvv,pin,expiryMonth,expiryYear) are sent as empty strings.
Response Data
{
"responseCode": "09",
"responseMessage": "Virtual account created successfully",
"authenticatePaymentResponseCode": "09",
"authenticatePaymentResponseMessage": "Virtual account created successfully",
"transactionId": "PP872639268879058968343",
"providerReference": "EDGL2632033431220261006125200",
"accountNumber": "6511101180",
"accountName": "ECHEZONA/Emino Sin Amor",
"isWebhookUrlSet": false,
"bankName": "Wema"
}
| Field | Type | Description |
|---|---|---|
responseCode | string | 09 means the virtual account was created. See Code Mappings. |
responseMessage | string | Human-readable description of the result. |
transactionId | string | The transaction this account is tied to. |
providerReference | string | Reference for the transaction on the payment provider's side. Store it for reconciliation and support requests. |
accountNumber | string | The virtual account number the customer should transfer to. |
accountName | string | The account name the customer will see when they enter the account number in their banking app. |
bankName | string | The bank the virtual account belongs to. |
isWebhookUrlSet | boolean | Whether your business has a webhook URL configured. If false, you won't receive a webhook for this payment — see Step 5. |
09 only means the account was generated. The customer hasn't paid yet. Don't fulfil the order until you've confirmed payment in Step 5.
Step 4: Display the Account and Start a Timer
Show the customer everything they need to complete the transfer:
- Bank name —
bankName - Account number —
accountNumber(add a copy button) - Account name —
accountName - Exact amount —
totalAmount_Transferfrom Step 2 - Time remaining — a countdown from 60 minutes
The account expires 1 hour after it is generated. The response doesn't include an expiry timestamp, so record the time you received the MakePayment response and count down from there.
const EXPIRY_MS = 60 * 60 * 1000; // 1 hour
function startCountdown(createdAt, onTick, onExpire) {
const expiresAt = createdAt + EXPIRY_MS;
const timer = setInterval(() => {
const remaining = expiresAt - Date.now();
if (remaining <= 0) {
clearInterval(timer);
onExpire();
return;
}
const minutes = Math.floor(remaining / 60000);
const seconds = Math.floor((remaining % 60000) / 1000);
onTick(`${minutes}:${String(seconds).padStart(2, "0")}`);
}, 1000);
return () => clearInterval(timer); // call to stop, e.g. once payment is confirmed
}
// Usage — record the time when MakePayment returns
const stop = startCountdown(
Date.now(),
(timeLeft) => (document.querySelector("#time-left").textContent = timeLeft),
() => showExpiredState() // hide the account details, offer "Generate new account"
);
When the timer runs out:
- Hide the account details so the customer can't pay into an expired account.
- Offer a Generate new account option. This starts the flow again from Step 1 with a new
transactionId— transaction IDs can't be reused (you'll get04 – Transaction id already exists).
If the customer refreshes the page, keep the countdown accurate by saving the creation time against the transactionId on your server and restoring the timer from it, rather than restarting at 60 minutes.
While the timer is running, you can have your server call Verify Payment (see Step 5) every 10–15 seconds so the page updates to "Payment received" as soon as the transfer lands.
Step 5: Confirm the Payment
There are two ways to learn that the customer has paid:
- Webhook — Echezona sends a
POSTto your webhook URL when the payment succeeds. See Webhooks. - Verify Payment — your server calls
Payments/VerifyPaymentwith thetransactionIdto get the current status.
We recommend using both. Use the webhook as the trigger, then verify the transaction before you fulfil the order. This protects you against spoofed or replayed webhook requests and confirms the amount you were paid.
Receive the Webhook
A webhook for a bank transfer looks like this:
{
"transactionId": "TXN123456789",
"amount": "5000.00",
"status": "Successful",
"paymentMethod": "Transfer",
"customerEmail": "customer@example.com",
"timestamp": "2026-10-06T12:52:00.0000000Z",
"currency": "NGN",
"metadata": "[{\"name\":\"OrderId\",\"value\":\"ORD987654321\"}]"
}
If the MakePayment response returns isWebhookUrlSet: false, no webhook will be sent. Set your webhook URL in Dashboard > Settings > API Keys > Webhook URL, or rely on calling Verify Payment periodically until the transaction succeeds or the account expires.
Verify the Transaction
When the webhook arrives, confirm the transaction status with Echezona. See Fetch Transaction Status for the full response reference.
Endpoint
POST Payments/VerifyPayment
Headers
Authorization: Bearer
To authorize this API request, you need to include your apiKey as a bearer token. This is done by setting the Authorization header in your request. The header should have the format:
Authorization: "Bearer your_api_key"
Note: Get your API key from Settings > API Keys in your dashboard.
Request Body
{
"transactionId": "TXN123456789"
}
Response Data
{
"responseCode": "00",
"responseMessage": "Transaction retrieved successfully",
"data": {
"amount": "5000.00",
"paymentType": "Transfer",
"currency": "NGN",
"status": "Successful",
"isSuccessful": true,
"gatewayResponse": "Transaction Successful. Approved by Financial Institution",
"transactionId": "TXN123456789",
"paymentReference": "EDGL2632033431220261006125200",
"merchantId": 12
}
}
Only treat the payment as successful if data.isSuccessful is true and data.amount matches what you expected. See Transaction Statuses.
Example: Webhook Handler
app.post("/webhooks/echezona", express.json(), async (req, res) => {
// Acknowledge immediately so the webhook isn't retried
res.sendStatus(200);
const { transactionId } = req.body;
// Don't trust the payload alone — confirm with Echezona
const response = await fetch("https://api.echezona.com/api/Payments/VerifyPayment", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ECHEZONA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ transactionId }),
});
const { data } = await response.json();
const order = await findOrderByTransactionId(transactionId);
if (!order || order.status === "paid") return; // unknown or already handled
if (data?.isSuccessful && Number(data.amount) === Number(order.amount)) {
await markOrderAsPaid(order.id);
}
});
Best Practices
- Use a new
transactionIdfor every account. Each virtual account is tied to one transaction, and IDs can't be reused. - Always verify server-side. Never fulfil an order based on the webhook payload or a client-side signal alone.
- Make fulfilment idempotent. Webhooks are retried up to three times, and you may also be polling — make sure an order is only fulfilled once.
- Show the exact amount. Display
totalAmount_Transferso the customer transfers the correct total. - Handle expiry. Hide expired accounts and let the customer generate a new one.
- Store
providerReference. It's what our support team will ask for if a transfer needs to be traced.
Related
- Initialize a Transaction
- Get Payment Details
- Make Payment
- Fetch Transaction Status
- Webhooks
- Code Mappings
Support
If you need assistance, reach out at hello@echezona.com.