Skip to content

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.


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 exactly

Store your credentials in server-only environment variables:

src/lib/esewa.ts
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"
};
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():

src/routes/checkout.ts
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: "..."
// }
// }
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+..."

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>

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:

  1. Validates the HMAC signature over the raw data string using crypto.timingSafeEqual.
  2. Normalizes comma-formatted amounts ("1,000.0" -> 100000 paisa).
  3. Queries eSewa’s S2S Status Endpoint (/api/epay/transaction/status/) to confirm that eSewa’s records show status: "COMPLETE" and match the order ID and amount.
src/routes/callback.ts
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:

  1. Signature Verification: Verifies the HMAC-SHA256 signature against the exact raw un-modified string ("1,000.0") as signed by eSewa.
  2. Paisa Normalization: Uses parseAmountToPaisa() to strip commas and safely parse into integer paisa (100000).
  3. Status Query Request: Normalizes the amount to clean decimal format ("1000.0") before invoking eSewa’s status API.

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: 9806800001 through 9806800005
  • Password: Nepal@123
  • MPIN: 1122
  • OTP: 123456