eSewa ePay v2
Complete reference for the eSewa ePay v2 gateway integration — flow, signature generation, verification, test credentials, and implementation details.
eSewa ePay v2
Audience: Backend developers, QA engineers Scope: eSewa ePay v2 integration details, HMAC signature, verification flow, test environment
Overview
eSewa is Nepal's most popular digital wallet. The ePay v2 API uses a form POST flow where the backend signs the payment fields and the frontend submits a hidden form to eSewa's portal.
| Property | Value |
|---|---|
| Gateway name | esewa |
| Initiation type | form_post |
| Signature algorithm | HMAC-SHA256 (base64) |
| Amount format | Rupees (string) |
| Official docs | developer.esewa.com.np/pages/Epay |
Payment Flow
Signature Generation
The backend signs three fields using HMAC-SHA256 with the merchant secret key:
const message = `total_amount=${totalAmount},transaction_uuid=${transactionUuid},product_code=${productCode}`;
const signature = createHmac("sha256", secretKey)
.update(message)
.digest("base64");The signed_field_names field tells both eSewa and the verification logic which fields were signed and in what order.
Form Fields (gatewayPayload)
These are the hidden form inputs the frontend submits to eSewa:
| Field | Description | Example |
|---|---|---|
amount | Payment amount (same as total_amount for simple payments) | "500" |
tax_amount | Tax component | "0" |
total_amount | Total amount including tax | "500" |
transaction_uuid | Unique transaction ID generated by backend | "42-1708000000000" |
product_code | Merchant/product code from eSewa | "EPAYTEST" |
product_service_charge | Service charge | "0" |
product_delivery_charge | Delivery charge | "0" |
success_url | Where eSewa redirects on success | "https://myapp.com/payment/success" |
failure_url | Where eSewa redirects on failure | "https://myapp.com/payment/failure" |
signed_field_names | Comma-separated list of signed fields | "total_amount,transaction_uuid,product_code" |
signature | HMAC-SHA256 signature (base64) | "kI4GnQx9..." |
Frontend Form Submission
const form = document.createElement("form");
form.method = "POST";
form.action = redirectUrl; // "https://rc-epay.esewa.com.np/api/epay/main/v2/form"
for (const [name, value] of Object.entries(gatewayPayload)) {
const input = document.createElement("input");
input.type = "hidden";
input.name = name;
input.value = value;
form.appendChild(input);
}
document.body.appendChild(form);
form.submit();Callback Response
After the user completes payment, eSewa redirects to success_url with a data query parameter containing a base64-encoded JSON string:
https://myapp.com/payment/success?data=eyJ0cmFuc2FjdGlvbl9jb2RlIjoi...Decoded JSON:
{
"transaction_code": "0005ABC",
"status": "COMPLETE",
"total_amount": "500",
"transaction_uuid": "42-1708000000000",
"product_code": "EPAYTEST",
"signed_field_names": "transaction_code,status,total_amount,transaction_uuid,product_code,signed_field_names",
"signature": "xyz789base64..."
}The backend decodes this payload on the API-owned success callback before redirecting the browser to the shared payment result page.
If PAYMENT_RESULT_PAGE_URL is configured, that result page can live in the frontend.
If it is not configured, the API temporarily falls back to:
${API_PUBLIC_BASE_URL}/api/payments/resultVerification (Two Steps)
Step 1: Signature Verification
The backend recomputes the HMAC using the signed_field_names from the response:
// Build message from signed_field_names in the response
const fields = signedFieldNames.split(",");
const message = fields.map((field) => `${field}=${response[field]}`).join(",");
const computed = createHmac("sha256", secretKey)
.update(message)
.digest("base64");
const isValid = computed === response.signature;This prevents tampering — if any signed field was modified, the signature won't match.
Step 2: Server-Side Status Check
Even if the signature is valid, the backend makes a server-to-server call to eSewa's status API for independent confirmation:
GET https://rc.esewa.com.np/api/epay/transaction/status/
?product_code=EPAYTEST
&total_amount=500
&transaction_uuid=42-1708000000000Expected response:
{
"status": "COMPLETE",
"ref_id": "0005ABC",
"total_amount": "500.0"
}The backend only marks the payment as completed if status === "COMPLETE".
Configuration
Environment Variables
| Variable | Description | Test Default |
|---|---|---|
ESEWA_EPAY_URL | eSewa form POST URL | https://rc-epay.esewa.com.np/api/epay/main/v2/form |
ESEWA_EPAY_STATUS_URL | eSewa status check API | https://rc.esewa.com.np/api/epay/transaction/status/ |
ESEWA_PRODUCT_CODE | Merchant/product code | EPAYTEST |
ESEWA_SECRET_KEY | HMAC secret key | 8gBm/:&EnhH.1/q |
These are defined in apps/api/src/config/env.validation.ts with Zod validation and test defaults.
Production
For production, replace the test values with real credentials from the eSewa merchant portal:
| Variable | Production Value |
|---|---|
ESEWA_EPAY_URL | https://epay.esewa.com.np/api/epay/main/v2/form |
ESEWA_EPAY_STATUS_URL | https://esewa.com.np/api/epay/transaction/status/ |
ESEWA_PRODUCT_CODE | Your registered merchant code |
ESEWA_SECRET_KEY | Your merchant secret key |
No code changes required — only env var values change.
Test Credentials
For the test/sandbox environment:
| Item | Value |
|---|---|
| eSewa Test IDs | 9806800001 through 9806800005 |
| Password | Nepal@123 |
| OTP Token | 123456 |
| MPIN (app only) | 1122 |
| Product Code | EPAYTEST |
| Secret Key | 8gBm/:&EnhH.1/q |
Implementation Reference
Gateway file: apps/api/src/modules/payment/gateways/esewa.gateway.ts
Key methods:
initiate()— generatestransaction_uuid, computes HMAC-SHA256 signature, returnsform_postpayloadverify()— validates response signature + calls eSewa status APIgenerateSignature()— HMAC-SHA256 helperverifyResponseSignature()— recomputes and compares HMAC from callbackcheckTransactionStatus()— server-to-server GET to eSewa status API
Security Checklist
- HMAC signature computed server-side — secret key never sent to frontend
- Callback signature verified before trusting response data
- Server-side status API call for independent confirmation
-
signed_field_namesfrom the response used for verification (not hardcoded) - Secret key stored in environment variable, not hardcoded
- Amount from eSewa's response returned for cross-checking by the consumer