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:
- Creating a checkout session: your server creates a session with the amount and customer details
- Redirecting to checkout: the customer pays on a hosted page
- Receiving a webhook: your server is notified of the payment outcome
- 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
| Event | When it fires |
|---|---|
payment.succeeded | Payment successful |
payment.failed | Payment declined or error |
payment.cancelled | Customer didn't pay within timeout |
payment.refunded | Refund fully processed |
payment_link.paid | A 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
- Read the full API reference
- See SDK guides for TypeScript, Python, and PHP
- For recurring billing use cases, see the subscription billing guide
- Get your API keys. Sandbox is free, no approval required