PayBridgeNP

PayBridgeNP

developerpayment-gatewaynepal

Verify Payments in Nepal: Webhooks, Not the Redirect

The right way to confirm eSewa, Khalti, and Fonepay payments: verify server-side, use signed webhooks, and never mark an order paid from the return URL.

July 18, 20264 min readPayBridgeNP Team
Verify Payments in Nepal: Webhooks, Not the Redirect

Here is a bug that ships to production more often than it should: an order gets marked "paid" the moment the customer lands back on the success page. It works in testing, so it goes live. Then someone opens the success URL directly, without paying, and gets a free order. This guide shows the correct way to confirm a payment in Nepal, whether you use eSewa, Khalti, or Fonepay.

The one rule: the redirect is not proof of payment

When a customer finishes at a payment provider, their browser is sent back to your site. That redirect is a user-interface event. It tells you the customer interacted with the payment page. It does not tell you money moved, and it can be triggered by anyone who knows the URL.

So the rule is simple: never release value from the return URL or from client-side JavaScript alone. Confirm the payment on your server first.

Confirm it server-side

There are two server-side signals you can trust, and you should use them together.

1. Read the payment back from the API. After the customer returns, fetch the payment on your server and check that it actually succeeded, and that the amount and currency match what you expected:

import { PayBridgeNP } from "@paybridge-np/sdk";

const paybridge = new PayBridgeNP({ apiKey: process.env.PAYBRIDGENP_API_KEY });

const payment = await paybridge.payments.retrieve(paymentId);

if (
  payment.status === "success" &&
  payment.amount === expectedAmount && // in paisa
  payment.currency === "NPR"
) {
  // Safe to fulfill the order
}

2. Listen for the webhook. PayBridgeNP sends a signed payment.succeeded event to your endpoint when a payment completes. Webhooks are the source of truth: they arrive even if the customer closes their browser before the redirect, which happens a lot on mobile.

Verify the webhook signature

Your webhook URL is public, so anyone can POST fake events to it. That is why every webhook is signed. PayBridgeNP sends an X-PayBridgeNP-Signature header shaped like t=1711234567,v1=<hmac>: a timestamp and an HMAC-SHA256 signature of the raw request body.

The SDK verifies it for you. Pass the raw request body, not a parsed object:

import { PayBridgeNP } from "@paybridge-np/sdk";

// In your webhook route
const event = await PayBridgeNP.webhooks.constructEvent(
  rawBody, // the raw string body, before any JSON parsing
  request.headers["x-paybridgenp-signature"],
  process.env.PAYBRIDGENP_WEBHOOK_SECRET, // whsec_...
);

if (event.type === "payment.succeeded") {
  // The signature is valid and the payment really succeeded
  // Now it is safe to fulfill the order
}

constructEvent also rejects events whose timestamp is more than five minutes old, which blocks replay attacks where someone captures a real webhook and sends it again later.

Verifying without the SDK

If you are not on the TypeScript SDK, the check is still short, and the webhook verification guide shows it in Python and PHP too. Recompute the HMAC over the timestamp and the raw body, then compare it to the v1 value with a timing-safe comparison:

import { createHmac, timingSafeEqual } from "node:crypto";

function isValidSignature(rawBody: string, header: string, secret: string) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const expected = createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`)
    .digest("hex");
  return timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

If verification keeps failing

The two usual causes, in order:

  • The body was changed before verifying. Frameworks often parse JSON for you and hand you an object. Re-serializing that object produces a slightly different string, so the signature no longer matches. Verify against the exact raw bytes you received.
  • The secret does not match. Sandbox and live each have their own signing secret. Make sure the whsec_ value you are verifying with belongs to the same mode as the event.

Why this matters more in Nepal

Wallet and QR flows lean heavily on mobile, where customers routinely close the browser before the redirect completes. If your order confirmation depends on that redirect, you will miss real payments and, worse, you will be exposed to fake ones. Confirming server-side with a signed webhook fixes both at once.

Next Steps


Related Articles

Ready to accept payments in Nepal?

Connect eSewa, Khalti, and Fonepay with a single API. Free to start, with no fund-holding and no new bank accounts.