esewa-npm
v1.0.0
Published
Zero-dependency Node.js SDK for the eSewa ePay v2 payment gateway (Nepal) — signature generation, checkout initialization and signed callback verification.
Downloads
24
Maintainers
Readme
esewa-npm
Zero-dependency Node.js SDK for the eSewa ePay v2 payment gateway (Nepal).
It implements the integration flow documented at developer.esewa.com.np: request signing, checkout initialization, and — critically — verification of the signed callback eSewa sends back to your success URL.
- Zero runtime dependencies — only
node:crypto - TypeScript-first — full
.d.tstypes ship in the package - Dual format — ESM + CommonJS builds with a proper
exportsmap - Node 18+ — uses native
fetch-era APIs; no polyfills
npm install esewa-npmThe payment flow in 30 seconds
┌──────────┐ 1. checkout.init() → signed form fields ┌──────────┐
│ Merchant │ ───────────────────────────────────────────► │ Browser │
│ Server │ 2. auto-submit POST form └────┬─────┘
└──────────┘ │ 3. POST /api/epay/main/v2/form
▼
┌──────────────┐
│ eSewa │ 4. customer logs in
│ ePay v2 │ (wallet / bank)
└──────┬───────┘
│ 5. redirect
▼
┌──────────┐ 6. GET {success_url}?data=<base64 JSON> ┌──────────────┐
│ Merchant │ ◄────────────────────────────────────────── │ eSewa │
│ Server │ 7. decode + VERIFY signature locally └──────────────┘
└──────────┘ 8. deliver goods only if verified === trueSteps 1–2 and 6–7 are what this SDK does. Never mark an order paid without step 7 — the callback data is client-controlled until its HMAC signature is verified with your secret key.
Quickstart (Express)
import express from 'express';
import { Esewa } from 'esewa-npm';
const esewa = new Esewa({
productCode: 'EPAYTEST', // your merchant code (EPAYTEST in UAT)
secretKey: '8gBm/:&EnhH.1/q', // your HMAC secret key (UAT key shown)
env: 'UAT', // 'UAT' | 'PROD'
});
const app = express();
app.use(express.urlencoded({ extended: true }));
// 1) "Pay with eSewa" — build the signed request and redirect the customer
app.get('/checkout/:orderId', (req, res) => {
const { htmlForm } = esewa.checkout.init({
amount: 100, // product price
taxAmount: 10, // defaults to 0
transactionUuid: req.params.orderId, // unique per attempt: [A-Za-z0-9-]
successUrl: 'https://shop.example.com/esewa/success',
failureUrl: 'https://shop.example.com/esewa/failure',
});
res.send(htmlForm); // auto-submitting form → redirects to eSewa
});
// 2) eSewa redirects back here on success: /esewa/success?data=<base64>
app.get('/esewa/success', (req, res) => {
const { data, verified } = esewa.checkout.parseCallback(String(req.query.data));
if (!verified) {
// Signature mismatch: someone tampered with the payload. Treat as fraud.
return res.status(400).send('Invalid eSewa signature');
}
if (data.status !== 'COMPLETE') {
return res.status(402).send(`Payment not complete: ${data.status}`);
}
// Verified: mark order paid using data.transaction_uuid + data.transaction_code
res.send(`Order ${data.transaction_uuid} paid. Ref: ${data.transaction_code}`);
});
// eSewa redirects here on failure / cancellation / pending
app.get('/esewa/failure', (req, res) => {
const { data, verified } = esewa.checkout.parseCallback(String(req.query.data));
res.send(`Payment failed${verified ? ` (${data.status})` : ''}`);
});
app.listen(3000);That's the entire integration. For server-rendered apps, write htmlForm to the response; for SPAs, return formFields as JSON and POST them from the browser to actionUrl.
async function payWithEsewa(orderId) {
const { actionUrl, formFields } = await fetch(`/api/checkout/${orderId}`).then(r => r.json());
const form = document.createElement('form');
form.action = actionUrl;
form.method = 'POST';
for (const [name, value] of Object.entries(formFields)) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = name;
input.value = value;
form.appendChild(input);
}
document.body.appendChild(form);
form.submit();
}Configuration
| Option | Type | Required | Description |
| ------------- | --------------------- | -------- | ---------------------------------------------------------------------------------------------------- |
| productCode | string | yes | Merchant code issued by eSewa. EPAYTEST for the sandbox. |
| secretKey | string | yes | HMAC secret key issued by eSewa. Keep this server-side only. |
| env | 'UAT' \| 'PROD' | no | Defaults to 'UAT'. Selects the endpoint set below. |
| successUrl | string | no | Default success redirect URL (can be overridden per request). |
| failureUrl | string | no | Default failure redirect URL (can be overridden per request). |
| hosts | Partial<EsewaHosts> | no | Advanced: override endpoint URLs. |
Endpoints
| Environment | Payment form endpoint |
| ----------- | ---------------------------------------------------- |
| UAT | https://rc-epay.esewa.com.np/api/epay/main/v2/form |
| PROD | https://epay.esewa.com.np/api/epay/main/v2/form |
Sandbox credentials (published by eSewa for testing)
| Item | Value |
| ------------- | -------------------------------------------------- |
| Product code | EPAYTEST |
| Secret key | 8gBm/:&EnhH.1/q |
| eSewa ID | 9711111111 … 9711111114 |
| Password | Nepal@123 |
| MPIN (app) | 1122 |
| OTP token | 123456 (fixed in UAT so you don't need real SMS) |
API reference
new Esewa(config) → Esewa
Creates a client bound to one merchant + environment. Throws EsewaValidationError when required config is missing or env is invalid.
esewa.checkout.init(params) → CheckoutInitResult
Builds and signs an ePay v2 payment request. No network call — returns:
| Property | Type | Description |
| ------------- | -------------------- | ----------------------------------------------------------------------- |
| actionUrl | string | Where the browser POST goes (per env). |
| formFields | CheckoutFormFields | Signed fields — POST them verbatim. |
| htmlForm | string | Self-submitting HTML form (noscript fallback included). |
| env | 'UAT' \| 'PROD' | Environment used. |
CheckoutParams:
| Param | Required | Notes |
| ----------------------- | -------- | ----------------------------------------------------------------------------------------------------------- |
| amount | yes | Product price. |
| taxAmount | no | Defaults 0. |
| productServiceCharge | no | Defaults 0. |
| productDeliveryCharge | no | Defaults 0. |
| totalAmount | no | Defaults to the sum of the four components. If provided and inconsistent, EsewaValidationError is thrown (eSewa defines total_amount = amount + tax + service + delivery). |
| transactionUuid | yes | Unique per request; alphanumeric + hyphen only (validated locally). |
| successUrl | no* | Required via config or param. |
| failureUrl | no* | Required via config or param. |
| signedFieldNames | no | Advanced override; defaults to total_amount,transaction_uuid,product_code. |
esewa.checkout.parseCallback(base64Data) → ParsedCallback
Decode and verify the data query parameter from your success/failure URL in one call. Returns { data, verified }.
- Throws
EsewaDecodeErrorwhen the payload is not base64-encoded JSON. - A bad signature does not throw — check
verified(that's the point: you want to inspect and log the fraudulent payload).
const { data, verified } = esewa.checkout.parseCallback(String(req.query.data));
// data.status: 'COMPLETE' | 'PENDING' | 'NOT_FOUND' | ... (when present)
// data.transaction_code: eSewa reference, e.g. '000AWEO'
// data.transaction_uuid: echoes what you sentesewa.checkout.decodeCallback(base64Data) → EsewaCallbackData
Decode only, no verification. Useful for logging raw payloads before deciding what to do.
esewa.checkout.verifyCallback(data, secretKey?) → boolean
Verify an already-decoded payload. Uses the client's secret key unless one is passed.
Low-level signature utilities
import { generateSignature, signFields, verifySignature, buildSignatureMessage } from 'esewa-npm';
// Raw HMAC-SHA256 (base64) over any message:
generateSignature('total_amount=110,transaction_uuid=241028,product_code=EPAYTEST', secret);
// → 'i94zsd3oXF6ZsSr/kGqT4sSzYQzjj1W/waxjWyRwaME=' (official docs vector)
// Sign a record of fields:
signFields({ total_amount: '110', transaction_uuid: '241028', product_code: 'EPAYTEST' }, secret,
'total_amount,transaction_uuid,product_code');
// Constant-time verification:
verifySignature(fields, signature, secret, signedFieldNames?);How the signature works
- Take the fields named in
signed_field_names, in that exact order. - Build the message
name1=value1,name2=value2,... signature = base64( HMAC-SHA256( message, secretKey ) )(RFC 2104).
Request side (you): signed_field_names = "total_amount,transaction_uuid,product_code" →
total_amount=110,transaction_uuid=241028,product_code=EPAYTESTResponse side (eSewa → you): the payload's own signed_field_names lists more fields, including signed_field_names itself as the last signed entry:
transaction_code=000AWEO,status=COMPLETE,total_amount=1000.0,
transaction_uuid=250610-162413,product_code=EPAYTEST,
signed_field_names=transaction_code,status,total_amount,transaction_uuid,product_code,signed_field_namesverifyCallback() handles both shapes automatically — you pass the raw fields and it reads signed_field_names from the payload. Verification is constant-time (timingSafeEqual), so it is safe against timing attacks.
Note: the standalone signature sample shown in the docs' Signature Generation section (
4Ov7pCI1…) does not reproduce from the printed input — it is a stale doc artifact. The form example (i94zsd3o…) and the callback response example do reproduce exactly, and this SDK matches both. Verified programmatically against the published vectors.
Error classes
| Class | Code | Raised when |
| ---------------------- | ----------------------- | ------------------------------------------------------------------ |
| EsewaError | — | Base class of all SDK errors. |
| EsewaValidationError | ESEWA_VALIDATION_FAILED | Bad config/params: missing fields, illegal transactionUuid, inconsistent totalAmount, … |
| EsewaDecodeError | ESEWA_DECODE_FAILED | Callback data is not base64-encoded JSON. |
All extend Error, so err instanceof EsewaError catches everything; err.code gives you the stable string.
Security checklist
- Never expose
secretKeyto the browser. Signing belongs on your server; the SPA pattern above fetches pre-signed fields from your API instead. - Only trust
verified === truepayloads. The unverifieddatais just base64 — anyone can craft it. - Cross-check the verified payload against your own records:
transaction_uuidmatches the pending order,total_amountmatches the order total, andproduct_codeis yours. Signature proves eSewa said it; your DB proves it's this order. - Deliver goods only after verification. If eSewa's response never arrives (session times out after 5 minutes), reconcile using eSewa's transaction status API — roadmap below.
- Keep
transactionUuidunique per attempt; reuse will collide in eSewa's ledger.
Roadmap
esewa.status.check(uuid)— transaction status API (/api/epay/transaction/status/)- Refund APIs
- Browser-safe build (client-side signing for non-hosted flows)
PRs welcome — the signature module is already generic enough to power all of them.
License
MIT
