By Muhammad Samu 8th Apr, 2026
The good news is that Pay with Transfer works across every integration path Monnify supports: the frontend One-Time Payment SDK, a server-side API transaction, and even a fully custom UI where you surface just the account number yourself.
This guide covers all three paths, explains how to get a specific virtual account number for a chosen bank, and shows you how to use the Monnify Bank Simulator to complete test transfers in sandbox without touching a real bank account.
Regardless of how a transaction is started, the core flow is the same:
In sandbox, step 3 is handled by the Monnify Bank Simulator instead of a real bank app. It works for every integration path described below.
If you are already using the Monnify One-Time Payment SDK to open the checkout modal, enabling Pay with Transfer requires no extra API calls. Just include ACCOUNT_TRANSFER in the paymentMethods array when you call MonnifySDK.initialize(). Monnify handles everything else — the virtual account is displayed inside the modal.
1MonnifySDK.initialize({
2 amount: 5000,
3 currency: "NGN",
4 reference: `txn_${Date.now()}`,
5 customerFullName: "Auwal MS",
6 customerEmail: "[email protected]",
7 apiKey: "YOUR_API_KEY",
8 contractCode: "YOUR_CONTRACT_CODE",
9 paymentDescription: "Order #1042",
10 paymentMethods: ["ACCOUNT_TRANSFER"], // show only the transfer tab
11 onLoadStart: () => console.log("Loading..."),
12 onLoadComplete: () => console.log("Ready"),
13 onComplete: (response) => {
14 // response.paymentStatus === "PAID" once the transfer is confirmed
15 console.log(response);
16 },
17 onClose: (data) => console.log("Modal closed", data),
18});When the modal opens, switch to the Pay with Transfer tab. You will see a bank name and account number. Use these in the Monnify Bank Simulator (Step 3) to complete the test transfer.
When you initiate the transaction from your server using the Initialize Transaction API, Monnify returns a checkoutUrl and a transactionReference. The checkout URL leads to the same hosted page as the SDK modal — opening it is the simplest way to show the customer their virtual account. You can also skip the checkout URL entirely and build your own account display, which is covered in Path 3.
1import fetch from "node-fetch";
2
3const MONNIFY_BASE = "https://sandbox.monnify.com";
4const API_KEY = process.env.MONNIFY_API_KEY;
5const SECRET = process.env.MONNIFY_SECRET;
6const CONTRACT = process.env.MONNIFY_CONTRACT_CODE;
7
8async function getAccessToken() {
9 const credentials = Buffer.from(`${API_KEY}:${SECRET}`).toString("base64");
10 const res = await fetch(`${MONNIFY_BASE}/api/v1/auth/login`, {
11 method: "POST",
12 headers: { Authorization: `Basic ${credentials}` },
13 });
14 const { responseBody } = await res.json();
15 return responseBody.accessToken;
16}
17
18async function initTransaction(amount, customerEmail, customerName) {
19 const token = await getAccessToken();
20
21 const payload = {
22 amount,
23 customerEmail,
24 customerName,
25 contractCode: CONTRACT,
26 paymentReference: `txn_${Date.now()}`,
27 currencyCode: "NGN",
28 paymentDescription: "Test Pay with Transfer",
29 redirectUrl: "https://yourdomain.com/payment/callback",
30 paymentMethods: ["ACCOUNT_TRANSFER"],
31 };
32
33 const res = await fetch(
34 `${MONNIFY_BASE}/api/v1/merchant/transactions/init-transaction`,
35 {
36 method: "POST",
37 headers: {
38 Authorization: `Bearer ${token}`,
39 "Content-Type": "application/json",
40 },
41 body: JSON.stringify(payload),
42 }
43 );
44
45 const { responseBody } = await res.json();
46 // responseBody.checkoutUrl → redirect or open this for the hosted page
47 // responseBody.transactionReference → keep this for verification & custom UI
48 return responseBody;
49}Redirect or link the customer to the checkoutUrl and they will see the virtual account details on the Monnify-hosted page. From there you follow the same simulator steps in Step 3.
Some businesses want full control over the payment UI — they only need the raw virtual account number so they can render it themselves. Monnify supports this through the Bank Transfer Init Payment endpoint.
After calling Initialize Transaction (Path 2) and obtaining the transactionReference, make a second call to:
POST /api/v1/merchant/bank-transfer/init-payment
Pass the transactionReference and the bankCode of the bank you want to issue the virtual account from. Monnify returns the account number, account name, and bank name you can display directly in your own UI — no Monnify-hosted page required.
1// Call this after initTransaction() to get a specific bank's virtual account.
2// bankCode examples: "50515" → Moniepoint MFB, "058" → GTBank, "044" → Access Bank, "232" → Sterling Bank
3async function getBankTransferAccount(transactionReference, bankCode) {
4 const token = await getAccessToken();
5
6 const res = await fetch(
7 `${MONNIFY_BASE}/api/v1/merchant/bank-transfer/init-payment`,
8 {
9 method: "POST",
10 headers: {
11 Authorization: `Bearer ${token}`,
12 "Content-Type": "application/json",
13 },
14 body: JSON.stringify({ transactionReference, bankCode }),
15 }
16 );
17
18 const { responseBody } = await res.json();
19 // responseBody.accountNumber → virtual account number to show the customer
20 // responseBody.accountName → account name
21 // responseBody.bankName → bank name
22 return responseBody;
23}
24
25// Example usage
26const { accountNumber, accountName, bankName } = await getBankTransferAccount(
27 "MNFY|20250408|000001",
28 "058" // GTBank
29);
30
31// Render these in your own UI:
32// "Transfer ₦5,000 to [accountNumber] ([bankName]) — [accountName]"Once you have a virtual account number — whether from the SDK modal, the Monnify checkout page, or your own custom UI — use the Monnify Bank Simulator to act as the customer and complete the transfer. This works for all three paths above.
/api/v1/merchant/bank-transfer/init-payment.PAID and dispatches the webhook.After the simulator posts the transfer, verify the payment server-side. There are two complementary approaches — use both for a robust integration.
Monnify POSTs a notification to your configured webhook URL when the payment is confirmed. Always validate the monnify-signature header before acting on the payload.
1import crypto from "crypto";
2import express from "express";
3
4const router = express.Router();
5const SECRET = process.env.MONNIFY_SECRET;
6
7router.post("/monnify/webhook", express.json(), (req, res) => {
8 const signature = req.headers["monnify-signature"];
9 const payload = JSON.stringify(req.body);
10
11 const expected = crypto
12 .createHmac("sha512", SECRET)
13 .update(payload)
14 .digest("hex");
15
16 if (signature !== expected) {
17 return res.status(401).send("Invalid signature");
18 }
19
20 const { paymentStatus, transactionReference, amountPaid } = req.body;
21
22 if (paymentStatus === "PAID") {
23 // Idempotency check: ensure you haven't already processed this reference
24 console.log(`Confirmed: ${transactionReference} — ₦${amountPaid}`);
25 // Fulfill order, credit wallet, etc.
26 }
27
28 // Always return 200 so Monnify stops retrying
29 res.sendStatus(200);
30});
31
32export default router;Use the Get Transaction Status endpoint to query payment status on demand — useful for confirming on a callback page or reconciling missed webhooks.
1async function verifyTransaction(transactionReference) {
2 const token = await getAccessToken();
3
4 const res = await fetch(
5 `${MONNIFY_BASE}/api/v2/transactions/${encodeURIComponent(transactionReference)}`,
6 { headers: { Authorization: `Bearer ${token}` } }
7 );
8
9 const { responseBody } = await res.json();
10 const { paymentStatus, amountPaid, paidOn } = responseBody;
11
12 console.log({ paymentStatus, amountPaid, paidOn });
13 // paymentStatus === "PAID" → safe to fulfill
14 return responseBody;
15}/success page. Always verify payment status server-side — via webhook or the status API — before fulfilling any order./api/v1/merchant/bank-transfer/init-payment. Each sandbox contract is assigned specific test banks.200 — Monnify retries on any other status./api/v1/merchant/bank-transfer/init-payment returns an error: Make sure the transactionReference is from a valid, unexpired transaction and that the bankCode you supplied is one of the banks available in your sandbox contract.https://sandbox.monnify.com during development — switch to https://api.monnify.com in production.paymentMethods: ["ACCOUNT_TRANSFER"] to restrict checkout to transfer only (applies to both SDK and API paths)./api/v1/merchant/bank-transfer/init-payment with your chosen bankCode and transactionReference to get the account details.monnify-signature header on every incoming webhook.Ready to go further? Read about securing and handling Monnify webhooks or explore the full Monnify API reference.