eSewa (ePay v2) Integration
eSewa is the most widely adopted digital payment wallet in Nepal. nepay supports the modern ePay v2 protocol, featuring cryptographic HMAC-SHA256 signatures, base64 callback payloads, and server-to-server status verification.
How ePay v2 Works
Section titled “How ePay v2 Works”1. Customer initiates checkout └─ Your Backend calls initiatePayment({ method: "esewa", ... }) └─ SDK formats amounts to decimal rupees and calculates HMAC-SHA256 signature └─ Returns form submission URL and fields object
2. Customer's Browser submits HTML Form POST to eSewa └─ eSewa prompts customer for wallet mobile number, password, and MPIN └─ Customer enters SMS OTP to authorize the debit
3. eSewa redirects Customer to success_url or failure_url └─ Payload attached as base64 string in ?data=... (GET) or form body (POST)
4. Your Backend verifies transaction └─ Decodes base64 JSON payload └─ Validates cryptographic signature with timing-safe comparison └─ Queries eSewa S2S Status Check API (/api/epay/transaction/status/) └─ Asserts status === "COMPLETE", ref_id is valid, and amount matches exactlyConfiguration & Credentials
Section titled “Configuration & Credentials”Store your credentials in server-only environment variables:
import type { EsewaConfig } 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: `${process.env.APP_URL}/api/payment/callback/esewa`, failureUrl: `${process.env.APP_URL}/api/payment/callback/esewa`, env: "sandbox", // "sandbox" | "production"};Configuration Parameters
Section titled “Configuration Parameters”| Property | Type | Requirement | Description |
|---|---|---|---|
productCode |
string | Required | Merchant identifier assigned by eSewa ("EPAYTEST" in UAT sandbox). |
secretKey |
string | Required | Secret key used to sign and verify HMAC-SHA256 digests. |
successUrl |
string | Required | The absolute URL eSewa redirects to when payment succeeds. |
failureUrl |
string | Required | The absolute URL eSewa redirects to if payment fails or is cancelled. |
env |
“sandbox” | “production” | Required | Switches between rc-epay.esewa.com.np and epay.esewa.com.np. |
Step 1: Initiating Payment (Generating Form Fields)
Section titled “Step 1: Initiating Payment (Generating Form Fields)”eSewa requires an HTML Form POST containing signed hidden inputs. Call initiatePayment():
import { initiatePayment } from "@bivekkk/nepay";import { esewaConfig } from "../lib/esewa.js";
const result = await initiatePayment({ method: "esewa", config: esewaConfig, order: { orderId: "INV-2024-001", // Must contain only alphanumeric and hyphen characters amountPaisa: 10000, // NPR 100.00 in integer paisa taxPaisa: 0, // Optional serviceChargePaisa: 0, // Optional deliveryChargePaisa: 0, // Optional },});
// result 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: "..."// }// }Form Fields Generated by SDK
Section titled “Form Fields Generated by SDK”| Field Name | Description | Example |
|---|---|---|
amount |
Subtotal in rupees formatted as decimal string | "100" |
tax_amount |
Applicable tax in rupees | "0" |
total_amount |
Total payable amount (amount + tax + service + delivery) |
"100" |
transaction_uuid |
Your unique order ID | "INV-2024-001" |
product_code |
Merchant product code | "EPAYTEST" |
signed_field_names |
CSV list of fields included in the HMAC digest | "total_amount,transaction_uuid,product_code" |
signature |
Cryptographic HMAC-SHA256 digest encoded in Base64 | "k9+..." |
Rendering Auto-Submitting HTML Form
Section titled “Rendering Auto-Submitting HTML Form”In your server route (e.g. Express or Next.js Route Handler), return an auto-submitting HTML page:
<!DOCTYPE html><html><head><title>Redirecting to eSewa...</title></head><body> <form id="esewa-form" method="POST" action="${result.url}"> ${Object.entries(result.fields).map(([k, v]) => `<input type="hidden" name="${k}" value="${v}" />`).join("")} </form> <script>document.getElementById("esewa-form").submit();</script></body></html>Step 2: Callback Handling (GET vs POST)
Section titled “Step 2: Callback Handling (GET vs POST)”Always read the data parameter from both query and body:
const rawData = (req.query.data || req.body?.data) as string;When decoded from Base64, the data parameter contains a JSON object:
{ "transaction_code": "0008FXY", "status": "COMPLETE", "total_amount": "1,000.0", "transaction_uuid": "INV-2024-001", "product_code": "EPAYTEST", "signed_field_names": "transaction_code,status,total_amount,transaction_uuid,product_code,signed_field_names", "signature": "dGVzdFNpZ25hdHVyZQ=="}Step 3: Server-to-Server Status Verification
Section titled “Step 3: Server-to-Server Status Verification”verifyPayment() executes a 3-tier validation:
- Validates the HMAC signature over the raw
datastring usingcrypto.timingSafeEqual. - Normalizes comma-formatted amounts (
"1,000.0"->100000paisa). - Queries eSewa’s S2S Status Endpoint (
/api/epay/transaction/status/) to confirm that eSewa’s records showstatus: "COMPLETE"and match the order ID and amount.
import { verifyPayment, EsewaRetryableError } from "@bivekkk/nepay";import { esewaConfig } from "../lib/esewa.js";
export async function handleEsewaCallback(req, res) { const data = (req.query.data || req.body?.data) as string;
try { const result = await verifyPayment({ method: "esewa", config: esewaConfig, data, expectedOrderId: "INV-2024-001", expectedAmountPaisa: 10000, });
if (result.status === "completed") { // Order is 100% verified server-to-server await db.orders.update({ where: { id: result.orderId }, data: { status: "PAID", gatewayRef: result.traceId }, }); return res.redirect(`/order/success?id=${result.orderId}`); }
return res.redirect(`/order/failed?reason=${encodeURIComponent(result.status)}`); } catch (error) { if (error instanceof EsewaRetryableError) { // eSewa's status endpoint returned 503 or transient network failure. // Do NOT cancel the order! Schedule a background worker to query status in 1-2 minutes. console.warn("eSewa status temporarily unavailable. Retrying later:", error.message); return res.redirect(`/order/pending?id=INV-2024-001`); }
// Cryptographic signature mismatch or amount tampering console.error("eSewa verification failed:", error); return res.redirect(`/order/failed?reason=verification_failed`); }}Edge Case: Handling eSewa Comma Strings ("1,000.0")
Section titled “Edge Case: Handling eSewa Comma Strings ("1,000.0")”eSewa has an established quirk: callback responses and status checks often format thousands with commas, e.g. "1,000.0".
If you perform standard numeric conversion (parseFloat("1,000.0")), JavaScript yields 1 (truncating at the comma)!
If you perform string equality ("1000.0" === "1,000.0"), it fails!
nepay solves this with a built-in multi-step algorithm:
- Signature Verification: Verifies the HMAC-SHA256 signature against the exact raw un-modified string (
"1,000.0") as signed by eSewa. - Paisa Normalization: Uses
parseAmountToPaisa()to strip commas and safely parse into integer paisa (100000). - Status Query Request: Normalizes the amount to clean decimal format (
"1000.0") before invoking eSewa’s status API.
Sandbox Testing Credentials (UAT)
Section titled “Sandbox Testing Credentials (UAT)”Use these credentials when testing on eSewa UAT (https://rc-epay.esewa.com.np):
- Product Code:
EPAYTEST - Secret Key:
8gBm/:&EnhH.1/q - Customer Test Wallets:
9806800001through9806800005 - Password:
Nepal@123 - MPIN:
1122 - OTP:
123456