Quick Start — eSewa & Khalti in 5 Minutes
Accepting online payments across Nepal requires handling two fundamentally different gateway architectures:
- eSewa (ePay v2): Requires an HTML Form POST containing an HMAC-SHA256 digital signature, redirecting the customer’s browser, and resolving a base64-encoded callback.
- Khalti (ePayment v2 / KPG-2): Requires a server-side JSON REST request to
/epayment/initiate/, redirecting the customer to a hostedpayment_url, and querying/epayment/lookup/with a payment index (pidx).
nepay unifies both behind a single, typesafe, server-side interface while enforcing strict integer paisa math and mandatory server-to-server verification.
1. Environment & Credentials Configuration
Section titled “1. Environment & Credentials Configuration”Payment credentials must remain strictly on your server. Never bundle them into frontend code. Store them in .env and load them on startup.
# eSewa Sandbox (UAT)ESEWA_PRODUCT_CODE="EPAYTEST"ESEWA_SECRET_KEY="8gBm/:&EnhH.1/q"ESEWA_ENV="sandbox"
# Khalti Sandbox# Create a free test account to get your keys: https://test-admin.khalti.com/#/join/merchantKHALTI_SECRET_KEY="your-khalti-sandbox-secret-key"KHALTI_ENV="sandbox"Initialize provider configurations in a server module:
import type { EsewaConfig, KhaltiConfig } from "@bivekkk/nepay";
export const esewaConfig: EsewaConfig = { productCode: process.env.ESEWA_PRODUCT_CODE ?? "EPAYTEST", secretKey: process.env.ESEWA_SECRET_KEY ?? "8gBm/:&EnhH.1/q", successUrl: "https://yourdomain.com/api/payment/callback/esewa", failureUrl: "https://yourdomain.com/api/payment/callback/esewa", env: "sandbox", // "sandbox" | "production"};
export const khaltiConfig: KhaltiConfig = { secretKey: process.env.KHALTI_SECRET_KEY!, websiteUrl: "https://yourdomain.com", returnUrl: "https://yourdomain.com/api/payment/callback/khalti", env: "sandbox", // "sandbox" | "production"};Configuration Parameters
Section titled “Configuration Parameters”| Field | Type | Required | Description |
|---|---|---|---|
productCode |
string | Required (eSewa) | Merchant code issued by eSewa ("EPAYTEST" for sandbox). |
secretKey |
string | Required | Cryptographic key used to sign eSewa digests or authenticate Khalti requests. |
successUrl / returnUrl |
string | Required | The URL on your server where the gateway redirects the customer after checkout. |
env |
“sandbox” | “production” | Required | Toggles between gateway test endpoints and live production servers. |
2. Initiating a Payment
Section titled “2. Initiating a Payment”Call initiatePayment() with method: "esewa" or method: "khalti". TypeScript automatically narrows the accepted options and return type.
Example: eSewa Initiation
Section titled “Example: eSewa Initiation”import { initiatePayment } from "@bivekkk/nepay";import { esewaConfig } from "../lib/payment.js";
const checkout = await initiatePayment({ method: "esewa", config: esewaConfig, order: { orderId: "INV-2024-001", amountPaisa: 10000, // NPR 100.00 },});
// checkout returns:// {// url: "https://rc-epay.esewa.com.np/api/epay/main/v2/form",// fields: {// amount: "100",// tax_amount: "0",// total_amount: "100",// transaction_uuid: "INV-2024-001",// product_code: "EPAYTEST",// signed_field_names: "total_amount,transaction_uuid,product_code",// signature: "dGVzdFNpZ25hdHVyZQ==",// success_url: "...",// failure_url: "..."// }// }Step-by-Step Breakdown:
Section titled “Step-by-Step Breakdown:”method: "esewa"informs the SDK to execute the eSewa v2 protocol.- The SDK converts
amountPaisa: 10000to the formatted rupee string"100"expected by eSewa’s form inputs. - It generates an HMAC-SHA256 signature using
config.secretKeyover the messagetotal_amount,transaction_uuid,product_code. - It returns
urlandfields. Your server renders an auto-submitting HTML form or returns this JSON to the frontend to POST to eSewa.
Example: Khalti Initiation
Section titled “Example: Khalti Initiation”import { initiatePayment } from "@bivekkk/nepay";import { khaltiConfig } from "../lib/payment.js";
const checkout = await initiatePayment({ method: "khalti", config: khaltiConfig, order: { orderId: "INV-2024-001", amountPaisa: 10000, // NPR 100.00 (min: 1000 paisa) purchaseOrderName: "Store Order #001", customerInfo: { name: "Ram Sharma", email: "ram@example.com", phone: "9800000000", }, },});
// checkout returns:// {// pidx: "53uiCQgY7Df2ZD7hmetofZ",// payment_url: "https://test-pay.khalti.com/?pidx=53uiCQgY7Df2ZD7hmetofZ",// expires_at: "2026-10-29T14:15:22.000000Z",// expires_in: 1800// }Step-by-Step Breakdown:
Section titled “Step-by-Step Breakdown:”method: "khalti"executes an authenticated REST call to Khalti’s/epayment/initiate/endpoint.order.amountPaisais passed directly as integer paisa (Khalti’s native format).- The response contains
pidx(payment index) andpayment_url. - Your server immediately redirects the customer’s browser to
payment_url.
3. Mandatory Server-to-Server Verification
Section titled “3. Mandatory Server-to-Server Verification”When the customer completes the transaction, they are redirected back to your callback route.
Verifying eSewa Callback
Section titled “Verifying eSewa Callback”When eSewa redirects back, it attaches a base64-encoded data parameter in the query or body:
import { verifyPayment } from "@bivekkk/nepay";import { esewaConfig } from "../lib/payment.js";
export async function handleEsewaCallback(req, res) { // In some environments eSewa delivers GET queries, in others POST form body const base64Data = (req.query.data || req.body.data) as string;
const result = await verifyPayment({ method: "esewa", config: esewaConfig, data: base64Data, expectedOrderId: "INV-2024-001", expectedAmountPaisa: 10000, });
if (result.status === "completed") { // 1. Transaction is cryptographically verified // 2. Gateway S2S status check confirmed "COMPLETE" // 3. Paid amount matched expectedAmountPaisa exactly await db.orders.update({ where: { id: result.orderId }, data: { status: "PAID", gatewayRef: result.traceId }, }); return res.redirect(`/order-confirmation?id=${result.orderId}`); }
// Handle failed or cancelled payment return res.redirect(`/payment-failed?reason=${encodeURIComponent(result.status)}`);}
---
### Verifying Khalti Callback
When Khalti redirects back to your `returnUrl`, it appends `?pidx=...&status=...`:
```ts title="src/routes/callback.ts"import { verifyPayment } from "@bivekkk/nepay";import { khaltiConfig } from "../lib/payment.js";
export async function handleKhaltiCallback(req, res) { const pidx = req.query.pidx as string;
const result = await verifyPayment({ method: "khalti", config: khaltiConfig, pidx: pidx, expectedOrderId: "INV-2024-001", expectedAmountPaisa: 10000, });
if (result.status === "completed") { // 1. Queried Khalti S2S /epayment/lookup/ // 2. Confirmed status === "Completed" // 3. Amount verified in paisa await db.orders.update({ where: { id: result.orderId }, data: { status: "PAID", gatewayRef: result.traceId }, }); return res.redirect(`/order-confirmation?id=${result.orderId}`); }
return res.redirect(`/payment-failed?reason=${encodeURIComponent(result.status)}`);}4. Understanding the PaymentResult Object
Section titled “4. Understanding the PaymentResult Object”Every verification returns a consistent PaymentResult object:
interface PaymentResult { status: "completed" | "pending" | "failed" | "expired" | "canceled"; amountPaisa: number; orderId: string; traceId: string | null; raw: unknown;}| Field | Type | Description |
|---|---|---|
status |
PaymentStatus | "completed" if payment is 100% verified server-to-server; "pending" if initiated but incomplete; "expired" if customer exceeded time; "canceled" if aborted; "failed" if tampered or amount mismatch. |
orderId |
string | The identifier of your order (transaction_uuid or purchase_order_id). |
amountPaisa |
number | Exact integer paisa verified by the gateway. |
traceId |
string | null | Gateway settlement reference (ref_id in eSewa, transaction_id in Khalti). |
raw |
unknown | The raw decoded response received from the gateway for debugging or logging. |