PayBridgeNPPayBridgeNP
developerpayment-gatewaynepaltutorial

Nepal Payment Gateway API: Integration Guide for Developers

A practical developer guide to integrating Nepal payment APIs: authentication, checkout sessions, webhooks, refunds, and the PayBridgeNP TypeScript SDK.

March 20, 20265 min readPayBridgeNP Team
Nepal Payment Gateway API: Integration Guide for Developers

If you're building a product for the Nepali market and need to accept payments, this guide covers everything you need as a developer. We'll cover the full integration lifecycle with working code examples.

Overview: What You're Building

A complete payment integration in Nepal typically involves:

  1. Creating a checkout session: your server creates a session with the amount and customer details
  2. Redirecting to checkout: the customer pays on a hosted page
  3. Receiving a webhook: your server is notified of the payment outcome
  4. Verifying and fulfilling: you verify the webhook signature and fulfill the order

This pattern is identical whether you're building with Node.js, Python, PHP, or any other backend.

Authentication

PayBridgeNP uses bearer token authentication. All API requests must include your secret key:

Authorization: Bearer sk_live_your_secret_key

Keep your secret key server-side only. Never expose it in client-side JavaScript, mobile apps, or public repositories.

Key types:

  • sk_live_...: live secret key (server-side only, real payments)
  • sk_test_...: sandbox secret key (server-side only; eSewa and Khalti sandbox move no real money)

Publishable keys (pk_...) are safe to use in client-side code; only secret keys (sk_...) must stay server-side.

Installing the SDK

TypeScript / Node.js

npm install @paybridge-np/sdk
# or
bun add @paybridge-np/sdk

Python

pip install paybridge-np

PHP

composer require paybridge-np/sdk

Creating a Checkout Session

This is the core operation. Your server creates a session; the customer completes payment on PayBridgeNP's hosted checkout.

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

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

// In your order handler:
const session = await paybridge.checkout.create({
  amount: 250000, // Amount in paisa (NPR 2,500.00)
  currency: "NPR",
  customer: {
    name: "Binod Karki",
    email: "binod@example.com",
    phone: "9851234567",
  },
  returnUrl: "https://yoursite.com/orders/success",
  cancelUrl: "https://yoursite.com/checkout",
  metadata: {
    orderId: "ORD-12345",
    userId: "usr_abc",
  },
});

// Redirect the customer
return redirect(session.checkout_url);

Amount is in paisa (1 NPR = 100 paisa). This is the standard for payment APIs: it avoids floating-point issues with decimal amounts.

Handling the Webhook

When the customer completes (or fails) payment, PayBridgeNP sends a POST request to your registered webhook URL.

import { PayBridgeNP } from "@paybridge-np/sdk";
import type { Request, Response } from "express";

// Shape of `data` on the payment.* events this endpoint handles.
type PaymentEventData = {
  metadata: Record<string, string>;
  amount: number;
  provider: "esewa" | "khalti" | "fonepay";
};

export async function handleWebhook(req: Request, res: Response) {
  const rawBody = req.rawBody; // raw body string, not parsed JSON
  const signature = req.headers["x-paybridgenp-signature"] as string;

  let event;
  try {
    // constructEvent verifies the HMAC-SHA256 signature (header format
    // `t=<timestamp>,v1=<hmac>`, signed over `${timestamp}.${body}`),
    // rejects timestamps older than 5 minutes, then returns the parsed
    // event. It throws if verification fails.
    event = await PayBridgeNP.webhooks.constructEvent<PaymentEventData>(
      rawBody,
      signature,
      process.env.PAYBRIDGENP_WEBHOOK_SECRET!,
    );
  } catch {
    return res.status(401).json({ error: "Invalid signature" });
  }

  switch (event.type) {
    case "payment.succeeded": {
      const { metadata, provider } = event.data;
      await fulfillOrder(metadata.orderId);
      console.log(`Order ${metadata.orderId} paid via ${provider}`);
      break;
    }
    case "payment.failed": {
      await markOrderFailed(event.data.metadata.orderId);
      break;
    }
    case "payment.cancelled": {
      await releaseInventory(event.data.metadata.orderId);
      break;
    }
  }

  return res.json({ received: true });
}

Always verify webhook signatures. Unverified webhooks are a security vulnerability: an attacker could POST fake "payment.succeeded" events to your endpoint.

Webhook Events Reference

EventWhen it fires
payment.succeededPayment successful
payment.failedPayment declined or error
payment.cancelledCustomer didn't pay within timeout
payment.refundedRefund fully processed
payment_link.paidA payment link was paid

Idempotency

Always design your fulfillment logic to be idempotent. Webhooks can be delivered more than once. Check if you've already fulfilled the order before acting:

case "payment.succeeded": {
  const order = await db.orders.findOne({ id: event.data.metadata.orderId });
  if (order.status === "fulfilled") break; // already handled
  await fulfillOrder(order.id);
  break;
}

Retrieving a Payment

You can fetch payment details at any time:

const payment = await paybridge.payments.retrieve("pay_abc123");
console.log(payment.status); // "pending" | "processing" | "success" | "failed" | "cancelled" | "refunded"
console.log(payment.provider); // "khalti" | "esewa" | "fonepay"
console.log(payment.amount); // 250000 (paisa)

Processing Refunds

const refund = await paybridge.refunds.create({
  paymentId: "pay_abc123",
  amount: 250000, // Full refund. Partial: specify less than original
  reason: "customer_request", // customer_request | duplicate | fraudulent | other
  notes: "Customer requested cancellation", // optional free-text note
});

console.log(refund.status); // "processing" | "succeeded" | "failed" | "requires_action"

Refunds are processed back to the original payment method. Processing time:

  • Khalti: usually fast (automated via API)
  • eSewa: typically a few business days, depending on the provider (manually processed by merchant)
  • Fonepay: typically a few business days, depending on the provider (requires merchant confirmation)

Sandbox Testing

Use sandbox keys (sk_test_...) for development. The sandbox behaves identically to production. eSewa and Khalti run against provider test environments, so no real money moves. Fonepay has no test environment: sandbox Fonepay uses your own Fonepay credentials and moves real money, capped at NPR 1,000 per payment and NPR 5,000 per month.

Sandbox test credentials are in your PayBridgeNP dashboard under Developers > API keys (switch to sandbox/test mode).

Error Handling

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

try {
  const session = await paybridge.checkout.create({ ... });
} catch (error) {
  if (error instanceof PayBridgeError) {
    console.error(error.statusCode); // HTTP status code
    console.error(error.code);       // "invalid_amount" | "provider_unavailable" | etc.
    console.error(error.message);    // Human-readable message
  }
}

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.