PayBridgeNPPayBridgeNP
payment-gatewaynepaldeveloper

Online Payment Security in Nepal: What Merchants Need to Know

A practical guide to payment security for Nepali merchants: verifying webhook signatures, HTTPS, protecting API keys, and how card data stays with PCI-compliant providers.

March 5, 20265 min readPayBridgeNP Team
Online Payment Security in Nepal: What Merchants Need to Know

Accepting online payments comes with security responsibilities. This guide covers the practical security measures every Nepali merchant should have in place, whether you're a developer building a custom integration or a business owner using a plugin.

The Most Common Security Mistakes in Nepal

Before diving into best practices, here are the mistakes we see most often:

  1. Verifying screenshots as payment proof: Easily faked. Never manually verify payments based on screenshots alone.
  2. Not verifying webhook signatures: Allows anyone to trigger fake payment notifications to your system.
  3. Hardcoding API keys in frontend code: Your secret key becomes public, anyone can make charges or access your data.
  4. Not using HTTPS: Payment data transmitted over HTTP is readable to network attackers.
  5. Trusting the amount from the redirect: Always verify the amount server-side via the API, not from URL parameters.

Always Use HTTPS

This is table stakes. Every page that handles payment data, including your order confirmation page and webhook endpoint, must be served over HTTPS.

Free SSL certificates are available via Let's Encrypt. Most hosting providers (SiteGround, Cloudflare, etc.) include SSL by default.

If your webhook endpoint isn't on HTTPS, PayBridgeNP will refuse to send events to it.

Verify Webhook Signatures

The most common security vulnerability in payment integrations is not verifying webhook signatures. Without verification, anyone can POST fake events to your webhook endpoint and trigger order fulfillment without payment.

PayBridgeNP signs every webhook with your webhook secret. Each delivery carries an X-PayBridgeNP-Signature header shaped like t=<timestamp>,v1=<signature>, where the signature is an HMAC-SHA256 over <timestamp>.<raw body> (the timestamp lets you reject replays):

import { createHmac, timingSafeEqual } from "crypto";

// PayBridgeNP sends the signature in an X-PayBridgeNP-Signature header,
// shaped like: t=<unix_seconds>,v1=<hmac_hex>
// The HMAC-SHA256 is computed over `${timestamp}.${rawBody}`.
function verifyWebhookSignature(
  rawBody: string,
  signatureHeader: string,
  secret: string
): boolean {
  const parts = Object.fromEntries(
    signatureHeader.split(",").map((p) => p.split("=") as [string, string])
  );
  const timestamp = parts["t"];
  const v1 = parts["v1"];
  if (!timestamp || !v1) return false;

  // Replay protection: reject signatures older than 5 minutes.
  const ageSeconds = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
  if (ageSeconds > 300) return false;

  const expected = createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");

  const received = Buffer.from(v1, "hex");
  const computed = Buffer.from(expected, "hex");
  return (
    received.length === computed.length &&
    timingSafeEqual(received, computed)
  );
}

// In your webhook handler:
const isValid = verifyWebhookSignature(
  req.rawBody,       // Must be the raw body string, not parsed JSON
  req.headers["x-paybridgenp-signature"],
  process.env.PAYBRIDGENP_WEBHOOK_SECRET!
);

if (!isValid) {
  return res.status(401).end(); // Reject invalid requests
}

If you use the official PayBridgeNP SDK, you don't need to write this yourself. await paybridge.webhooks.constructEvent(rawBody, req.headers["x-paybridgenp-signature"], secret) verifies the signature and the replay window for you and returns the parsed event, throwing if the signature is invalid.

Important: You need the raw request body (before JSON parsing) for signature verification. In Express, use express.raw() on your webhook route, not express.json().

Keep API Keys Server-Side

Your PayBridgeNP secret key (sk_live_...) must never appear in:

  • Client-side JavaScript
  • Mobile app source code
  • Public GitHub repositories
  • Browser DevTools

If a secret key is compromised, rotate it immediately in your PayBridgeNP dashboard.

What's never safe to expose:

  • Your secret key (sk_live_... or sk_test_...) must remain server-side only. Publishable keys (pk_...) are the client-safe counterpart, so only the secret key needs to stay hidden.

Use environment variables:

# .env (never commit this file)
PAYBRIDGENP_API_KEY=sk_live_your_key_here
PAYBRIDGENP_WEBHOOK_SECRET=whsec_your_secret_here
// Reference via environment variables only
const paybridge = new PayBridgeNP({
  apiKey: process.env.PAYBRIDGENP_API_KEY!,
});

Verify Payment Amounts Server-Side

Never trust the amount from a redirect URL or client-side code. Always verify via the API:

// BAD: trusting amount from URL parameter
const paidAmount = req.query.amount; // attacker can modify this!

// GOOD: verify via API
const payment = await paybridge.payments.retrieve(paymentId);
if (payment.amount !== expectedAmount) {
  throw new Error("Payment amount mismatch");
}

Idempotent Order Fulfillment

Handle duplicate webhook deliveries gracefully. PayBridgeNP guarantees at-least-once delivery, meaning you may receive the same event more than once:

case "payment.succeeded": {
  const order = await db.orders.findById(event.data.metadata.orderId);
  
  // Check before acting
  if (order.status === "paid" || order.status === "fulfilled") {
    return res.json({ received: true }); // Already handled, no-op
  }
  
  await db.orders.markAsPaid(order.id, event.data.id);
  await fulfillOrder(order.id);
  break;
}

Protect Your Admin Panel

Your PayBridgeNP dashboard has full access to your payment data and can trigger refunds. Protect it:

  1. Use a strong, unique password
  2. Sign in with a passkey or Google rather than a password you reuse elsewhere
  3. Don't share access credentials
  4. Review API key access regularly in the Keys section of the dashboard

What PayBridgeNP Handles For You

PayBridgeNP is designed to handle the hardest security problems so you don't have to:

  • PCI scope: Card and wallet details are entered on the provider's (eSewa, Khalti, Fonepay) own PCI DSS systems, never on your servers or PayBridgeNP's. PayBridgeNP stays out of card-data and PCI scope.
  • Provider authentication: Your provider credentials (eSewa, Khalti, Fonepay) are encrypted at rest with AES-GCM
  • Abuse controls: Server-side rate limits and abuse protection on the API
  • TLS 1.2+ enforcement: All API traffic is encrypted in transit
  • Signed webhooks: HMAC-SHA256 signatures on every event

Security Checklist for Merchants

Before going live, verify:

  • HTTPS enabled on your site
  • Webhook signature verification implemented
  • API secret key in environment variable, not hardcoded
  • Payment amounts verified server-side, not from client/URL
  • Order fulfillment is idempotent (handles duplicate webhooks)
  • Webhook endpoint returns 200 quickly (don't do slow work before responding)
  • Test webhook delivery in PayBridgeNP dashboard

For developers building on PayBridgeNP, our security policy has more depth. If you're using the WooCommerce checkout, the plugin handles webhook registration automatically. For custom API integration, implement the checklist above before going live. Questions? Email support@paybridgenp.com.


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.