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
- Test your endpoint with the webhook debugger before you go live.
- Read the developer API guide for the full payment and webhook shapes.
- See how to integrate Fonepay alongside eSewa and Khalti through one API.
- Get your API keys in the PayBridgeNP dashboard and test webhooks in sandbox for free.