Take a customer from "Add to Cart" to a verified payment — including the failure cases the quickstart doesn't walk you through
You're building a checkout for an online store. A customer fills their cart, clicks "Pay," and from that moment a dozen things can happen, most of them fine, some of them not. They complete payment cleanly. They close the tab mid-OTP. Their card gets declined. Your callback fires but the money isn't actually real. A good checkout handles all of these, not just the success path.
This guide builds a complete e-commerce checkout on the Interswitch Payment Gateway using Web Checkout, then walks through the failure cases that separate a checkout that works in a demo from one that survives real customers.
A quick note on terminology before we start: Interswitch's Web Checkout has two integration methods: Inline Checkout (the payment popup opens over your page, no redirect) and Web Redirect (the customer is sent to Interswitch's payment page and back). We'll use Inline Checkout, since keeping the customer on your store is the better experience for e-commerce.
What We're Building
A checkout flow for an online store that:
- Takes the customer's cart total and triggers an Interswitch payment
- Handles the customer completing payment and the ways they don't
- Verifies the payment server-side before fulfilling the order (the part that matters most)
- Moves cleanly from test to production
Step 1 — Test Credentials
| Parameter | Value |
|---|---|
| Merchant Code | MX6072 |
| Pay Item ID | 9405967 |
| Client ID | IKIAB23A4E2756605C1ABC33CE3C287E27267F660D61 |
| Secret | secret |
| Mode | TEST |
When you go live, you'll generate your own.
Step 2 — Trigger Checkout From The Cart
For an e-commerce store, Inline Checkout is the right choice. The payment popup opens over your store, the customer never leaves your page, and the cart context stays intact.
Add the script:
<script src="https://newwebpay-sandbox.interswitchng.com/inline-checkout.js"></script>
Now wire it to your cart. The key move here is translating your cart total into what Interswitch expects:
function checkoutCart(cart) {
// cart.total is in naira (e.g. 4500.00)
// Interswitch expects kobo, so multiply by 100
const amountInKobo = Math.round(cart.total * 100);
// Generate a unique reference and store it against this order
const txnRef = `order_${cart.orderId}_${Date.now()}`;
window.webpayCheckout({
merchant_code: "MX6072",
pay_item_id: "9405967",
txn_ref: txnRef,
amount: amountInKobo,
currency: 566, // NGN
cust_email: cart.customerEmail,
cust_name: cart.customerName,
site_redirect_url: "https://yourstore.com/payment-complete", // optional for inline; used by web redirect
onComplete: (response) => handlePaymentResponse(response, cart.orderId, txnRef),
mode: 'TEST'
});
}
Two things here are the source of most real bugs, so take note of them:
Amount is in kobo. A ₦4,500 cart is 450000, not 4500. Convert at the boundary — Math.round(cart.total * 100) and never let naira leak into the API call.
txn_ref must be unique per attempt. Notice it includes both the order ID and a timestamp. The order ID alone isn't enough, because a customer might try to pay for the same order twice (first attempt failed, they retry). Each attempt needs its own reference, while still being traceable back to the order.
Step 3 — Handle What The Customer Actually Does
The quickstart shows you the success case. With real customers, it gets trickier. Here's the response handler that deals with reality:
function handlePaymentResponse(response, orderId, txnRef) {
// resp === "00" indicates the customer completed the flow in the browser.
// (Confirm the callback object's exact field names against a live test —
// see the note below.)
if (response.resp === "00") {
// Customer completed the flow — but DON'T fulfill yet.
// Send to your server to verify before giving value.
verifyAndFulfill(orderId, txnRef);
} else {
// Customer did not complete — declined, cancelled, or errored.
// Do NOT fulfill. Show a retry option.
showRetryOption(orderId, response.desc);
}
}
A note on field names: Interswitch's Web Redirect response is documented to return resp (response code — 00 means approved), desc (description), apprAmt (approved amount), txnref, and retRef. The Inline onComplete callback receives a response object that mirrors these, but the docs don't enumerate its fields explicitly, so console.log(response) once in the sandbox and confirm the exact keys before you rely on them. Whatever the field names, the rule in Step 4 is unchanged: the callback is never your source of truth.
But there's a case the response handler alone can't catch: the customer who closes the tab mid-payment. If a customer starts the OTP step and then closes the popup, your onComplete callback may never fire. As far as your frontend knows, nothing happened, but the payment might have actually gone through on Interswitch's side.
This is exactly why you cannot rely on the callback as your source of truth, and why the next step exists.
Step 4 — Verify Server-Side (The Rule You Never Break)
This is the most important section in this guide.
Never fulfill an order based on the client-side callback alone.
The callback firing with a success code means the customer completed the payment flow in their browser. It does not, by itself, prove the money is real and in your account. Two reasons this matters for an e-commerce store specifically-
First, the abandoned-tab problem from above - the callback might never fire even when payment succeeded. Your server needs to be able to check independently.
Second, client responses can be manipulated. A malicious user can fake a success response to your frontend. If you ship goods on that alone, you've shipped for free.
So before you mark the order paid and trigger fulfillment, your server independently verifies the transaction with Interswitch using your merchant code, the transaction reference you stored, and the amount:
// On your server — verify before fulfilling
async function verifyAndFulfill(orderId, txnRef, expectedAmountInKobo) {
const merchantCode = "MX6072";
const response = await fetch(
`https://sandbox.interswitchng.com/collections/api/v1/gettransaction.json` +
`?merchantcode=${merchantCode}` +
`&transactionreference=${txnRef}` +
`&amount=${expectedAmountInKobo}`,
{
method: 'GET',
headers: { 'Content-Type': 'application/json' }
}
);
const result = await response.json();
// ResponseCode "00" = approved. Always confirm the returned
// Amount matches your order total before giving value.
const isApproved = result.ResponseCode === "00";
const amountMatches = Number(result.Amount) === expectedAmountInKobo;
if (isApproved && amountMatches) {
markOrderPaid(orderId);
triggerFulfillment(orderId);
} else {
flagForReview(orderId, result.ResponseDescription);
}
}
Two things to note about this verification call:
Auth on the requery. The simplified call above passes only the query parameters. Some Interswitch setups require an additional Hash header on the gettransaction.json requery (a SHA-512 hash of the requery string plus your secret key). Confirm against your Developer Console whether your integration needs it, and add it before going live if so.
The returned Amount is also in kobo. You're comparing your expectedAmountInKobo against Interswitch's Amount, both in the minor unit so don't accidentally compare naira to kobo and trigger a false mismatch.
Check the amount, not just the status. Verify not only that a payment happened, but that the amount paid matches the order total. A customer paying ₦100 for a ₦45,000 order is a payment that "succeeded" and one you must never fulfill. Confirm both that the money is real and that it's the right money.
The flow is always: customer's browser says "I think I paid" → your server confirms "yes, the money is real and it's the correct amount" → only then do you give value. Build this from day one. It's far harder to retrofit after you've been fulfilling on trust.
Step 5 — Complete a Test Payment
With the flow wired up, use Interswitch's test cards to run a payment in sandbox. Enter a test card in the popup, complete the OTP, and watch your handler fire. Then deliberately test the failure paths: cancel mid-flow, close the tab, use a declining test card. A checkout you've only tested on the success path is a checkout you haven't really tested.
Step 6 — Go Live
When test works and the failure cases are handled:
- Generate your own credentials and replace the sandbox ones
- Change the sandbox host to the live host (script: newwebpay.interswitchng.com; verification: webpay.interswitchng.com)
- Set mode: 'LIVE'
The code doesn't change. only credentials, URL, and mode. What you tested is what ships.
A note on base URLs: Interswitch uses a few sandbox hostnames across their docs and plugins (sandbox.interswitchng.com, newwebpay-sandbox.interswitchng.com, and others). The ones used here match the current Web Checkout documentation, but always confirm the exact base URLs against your Developer Console before going live.
A Note on Transaction Verification
For the server-side verification step, you use the gettransaction.json endpoint to confirm status with Interswitch. One thing worth knowing as you architect this: the Transaction Search API does not currently cover IPG payments, so your verification uses IPG's own transaction status check (shown above), not Transaction Search. Keep those two systems distinct, they solve different problems.
Wrapping Up...
A real e-commerce checkout isn't the success path; it's the success path plus every way a customer can deviate from it. We built a flow that converts the cart correctly (kobo, unique references), handles the customer who completes payment and the one who closes the tab, and most importantly, verifies server-side that the money is real and the amount is right before shipping a single item.
The one rule, again, because it's the one that matters: the browser says "I paid," your server confirms "the money is real and correct," and only then do you fulfill.
Resources
- QuickStart — Accept Your First Payment
- Web Checkout
- Default Test Credentials
- Getting Integration Credentials
Building checkout with Interswitch? Join the Interswitch Developer Community on Slack, share what you're building and get help from developers who've shipped it.

Top comments (0)