By Chidubem Agulue 7th Jan, 2026
Monnify is one of the most popular payment platforms for Nigerian businesses, and it offers two distinct ways to accept card payments: a hosted checkout that abstracts away most of the complexity, and a set of APIs that allow you charge cards directly and handle OTP and 3-D Secure flows yourself.
In this article, we’ll walk through both approaches. You’ll learn when to use Monnify’s Checkout SDK, when a direct card charge integration makes sense, and how to correctly handle OTP and 3-D Secure authorization without breaking the user experience.
checkoutUrl in the response object (or you can use the inline SDK). Note that to restrict the payment method to cards only, the paymentMethods array in the request body should contain only CARDS.checkoutUrl from the user’s browser. The modal/page handles card entry, 3DS, bank flows, authorization, etc.redirectUrl of your request body and emits webhook events. You can also verify final status via the transaction status endpoint.1import express from "express";
2import fetch from "node-fetch";
3const router = express.Router();
4
5const MONNIFY_BASE = "https://sandbox.monnify.com"; // change to live on production
6const API_KEY = process.env.MONNIFY_API_KEY;
7const SECRET = process.env.MONNIFY_SECRET;
8const CONTRACT_CODE = process.env.MONNIFY_CONTRACT_CODE;
9
10async function getAccessToken() {
11 const basic = Buffer.from(`${API_KEY}:${SECRET}`).toString("base64");
12 const res = await fetch(`${MONNIFY_BASE}/api/v1/auth/login`, {
13 method: "POST",
14 headers: { Authorization: `Basic ${basic}`, "Content-Type": "application/json" },
15 });
16 const body = await res.json();
17 return body.responseBody.accessToken;
18}
19
20router.post("/init", async (req, res) => {
21 const { amount, customerName, customerEmail } = req.body;
22 const token = await getAccessToken();
23 const payload = {
24 amount,
25 customerName,
26 customerEmail,
27 paymentReference: `order_${Date.now()}`,
28 contractCode: CONTRACT_CODE,
29 redirectUrl: "https://yourdomain.com/monnify/callback"
30 };
31 const r = await fetch(`${MONNIFY_BASE}/api/v1/merchant/transactions/init-transaction`, {
32 method: "POST",
33 headers: {
34 Authorization: `Bearer ${token}`,
35 "Content-Type": "application/json"
36 },
37 body: JSON.stringify(payload),
38 });
39 const data = await r.json();
40 // data.responseBody.checkoutUrl holds the URL
41 res.json(data.responseBody);
42});
43
44export default router;
45 MonnifySDK.initialize() option that opens a modal — see Monnify SDK docs.transactionReference that was generated from step 1.1router.post("/direct-charge", async (req, res) => {
2 const token = await getAccessToken();
3 const payload = {
4 transactionReference: req.body.transactionReference,
5 card: {
6 number: req.body.cardNumber,
7 expiryMonth: req.body.expiryMonth,
8 expiryYear: req.body.expiryYear,
9 pin: req.body.pin,
10 cvv: req.body.cvv,
11 }
12 };
13
14 const response = await fetch(`${MONNIFY_BASE}/api/v1/merchant/cards/charge`, {
15 method: "POST",
16 headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" },
17 body: JSON.stringify(payload),
18 });
19
20 res.json(await response.json());
21});status returned by the API determines your next step:SUCCESS: Payment complete.OTP_REQUIRED: Show an input field for the user to enter the code sent to their phone. Then call the authorize-otp endpoint.1// POST /api/monnify/authorize-otp
2fetch(`${MONNIFY_BASE}/api/v1/merchant/cards/otp/authorize`, {
3 method: "POST",
4 headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
5 body: JSON.stringify({
6 transactionReference: "<reference-from-charge-response>",
7 tokenId: "<reference-from-charge-response>",
8 otp: "<user-typed-otp>"
9 })
10});BANK_AUTHORIZATION_REQUIRED: This triggers 3D Secure (3DS). acsUrl. You must perform a full browser redirect to that URL. Monnify will display the bank's authentication page.1async function handleChargeResponse(data) {
2 const { status, secure3dData, transactionReference } = data.responseBody;
3
4 if (status === "BANK_AUTHORIZATION_REQUIRED") {
5 // Redirect browser to bank's 3DS page
6 window.location.href = secure3dData.acsUrl;
7 } else if (status === "OTP_REQUIRED") {
8 // Open your custom OTP modal
9 promptUserForOTP(transactionReference);
10 } else if (status === "SUCCESS") {
11 // verify server-side and show success
12 }
13}