Testing Pay with Transfer on Monnify Sandbox

By Muhammad Samu 8th Apr, 2026

Pay with Transfer is one of the most popular payment methods on Monnify. Instead of entering card details, a customer receives a virtual bank account number, makes a transfer from their mobile banking app, and the payment is confirmed instantly — no redirects, no OTP prompts, no card declines.

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.

How Pay with Transfer Works

Regardless of how a transaction is started, the core flow is the same:

  1. A transaction is created and Monnify generates a virtual account number tied to it.
  2. The virtual account number (and its bank) is shown to the customer.
  3. The customer makes a transfer to that account for the exact amount.
  4. Monnify detects the transfer and fires a webhook to your server.
  5. Your server validates the webhook and fulfills the order.

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.

Prerequisites

  1. A Monnify sandbox account. Sign up here if you haven't already.
  2. Your sandbox API Key, Secret Key, and Contract Code from the Monnify dashboard under Developer > API Keys & Contracts.
  3. A webhook URL configured in your dashboard under Settings > Webhook URL. You can use ngrok or smee.io to expose a local server during development.

Path 1 — One-Time Payment SDK (Frontend)

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.

checkout.js (frontend)
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.

Path 2 — Initialize Transaction API (Backend)

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.

initTransaction.js (server)
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.

Path 3 — Custom Account Display (Skip the Checkout URL)

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.

getBankTransferAccount.js (server)
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]"

Step 3 — Simulate the Transfer with the Monnify Bank Simulator

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.

  1. Open the simulator: Navigate to https://websim.sdk.monnify.com/?#/bankingapp. You will see an interface resembling a Nigerian mobile banking app.
  2. Click "Transfer": Click the Transfer option in the simulator's navigation.
  3. Enter the bank: Select the same bank shown on your checkout page or returned by /api/v1/merchant/bank-transfer/init-payment.
  4. Enter the account number: Paste the virtual account number displayed to the customer.
  5. Enter the amount: Type the exact amount specified when the transaction was initialized. Monnify matches transfers by both account number and amount.
  6. Submit the transfer: Click Make Payment. The simulator notifies Monnify's sandbox infrastructure, which marks the transaction as PAID and dispatches the webhook.

Step 4 — Verify the Payment

After the simulator posts the transfer, verify the payment server-side. There are two complementary approaches — use both for a robust integration.

Option A — Webhook (recommended)

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.

webhook.js (server)
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;

Option B — Transaction Status API

Use the Get Transaction Status endpoint to query payment status on demand — useful for confirming on a callback page or reconciling missed webhooks.

verifyTransaction.js (server)
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}

Troubleshooting Common Issues

  1. Transfer submitted but status stays pending: Confirm you entered the exact amount in the simulator. A mismatch is treated as an over- or underpayment and won't auto-complete the transaction.
  2. Wrong bank in the simulator: Select the bank shown on the checkout page or returned by /api/v1/merchant/bank-transfer/init-payment. Each sandbox contract is assigned specific test banks.
  3. Webhook not received: Ensure your webhook URL is publicly reachable (use ngrok locally) and is saved in your Monnify dashboard under Settings > Webhook URL. Check that your server returns 200 — Monnify retries on any other status.
  4. /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.
  5. Expired transaction: Checkout sessions have a limited validity window (Defaults to 40 mins). Initialize a fresh transaction and repeat from Step 1.

Quick Checklist

  1. Use https://sandbox.monnify.com during development — switch to https://api.monnify.com in production.
  2. Set paymentMethods: ["ACCOUNT_TRANSFER"] to restrict checkout to transfer only (applies to both SDK and API paths).
  3. If building a custom UI, call /api/v1/merchant/bank-transfer/init-payment with your chosen bankCode and transactionReference to get the account details.
  4. Use the Monnify Bank Simulator to complete test transfers — works for SDK, API, and custom UI paths.
  5. Enter the exact amount in the simulator to avoid partial-payment scenarios.
  6. Validate the monnify-signature header on every incoming webhook.
  7. Make your webhook handler idempotent — Monnify may deliver the same event more than once.
  8. Always verify payment status server-side before fulfilling an order.

Summary

Pay with Transfer on Monnify works across every integration path — the One-Time Payment SDK for a quick frontend drop-in, the Initialize Transaction API for server-driven flows, and /api/v1/merchant/bank-transfer/init-payment when you want to surface a specific bank's virtual account inside your own UI. In sandbox, the Monnify Bank Simulator stands in for the customer's banking app and works identically across all three paths. Get the simulator flow solid in sandbox, validate your webhooks, and your live integration will behave exactly the same way.

Ready to go further? Read about securing and handling Monnify webhooks or explore the full Monnify API reference.

Copyright © 2026 Monnify
instagramfacebookicon