Skip to main content

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.

Relevant Licenses & Compliance Certifications Required

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​

  1. Initialize the transaction with your API key and get an accessCode.
  2. Get Payment Info with the accessCode to get the token that authenticates the next call.
  3. Make Payment with paymentType: 5 (Bank Transfer) to generate the virtual account.
  4. Display the account details to your customer with a 1-hour countdown.
  5. 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​

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​

Response (200)
{
"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:

FieldTypeDescription
tokenstringAuthenticates the MakePayment call in the next step (instead of your API key).
transferPaymentbooleanMust be true — confirms bank transfer is enabled for your business.
totalAmount_TransfernumberThe total the customer should transfer, including any fees. See Fees & Amounts.
Response (200) — trimmed
{
"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​

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"
}
}
  • paymentType is 5 for Bank Transfer. See Code Mappings.
  • transactionId must 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​

Response (200)
{
"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"
}
FieldTypeDescription
responseCodestring09 means the virtual account was created. See Code Mappings.
responseMessagestringHuman-readable description of the result.
transactionIdstringThe transaction this account is tied to.
providerReferencestringReference for the transaction on the payment provider's side. Store it for reconciliation and support requests.
accountNumberstringThe virtual account number the customer should transfer to.
accountNamestringThe account name the customer will see when they enter the account number in their banking app.
bankNamestringThe bank the virtual account belongs to.
isWebhookUrlSetbooleanWhether your business has a webhook URL configured. If false, you won't receive a webhook for this payment — see Step 5.
Response code 09 is not a successful payment

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_Transfer from 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.

countdown.js
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 get 04 – Transaction id already exists).
Store the timestamp server-side too

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 POST to your webhook URL when the payment succeeds. See Webhooks.
  • Verify Payment — your server calls Payments/VerifyPayment with the transactionId to 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:

Example Payload
{
"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\"}]"
}
isWebhookUrlSet is false?

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​

Request
{
"transactionId": "TXN123456789"
}

Response Data​

Response (200)
{
"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​

webhook-handler.js (Node.js / Express)
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 transactionId for 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_Transfer so 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.

Support​

If you need assistance, reach out at hello@echezona.com.