Praxium Labs ships this for Nepali clients — here is what works. eSewa's documentation is solid but spread thin. This post gives you the integration in three languages so you can copy, paste, and ship in a morning.
Test credentials (eSewa UAT)
Use these for all development. Switch to production credentials only when going live:
- Merchant Code: EPAYTEST
- Secret Key: 8gBm/:&EnhH.1/q (test-environment only)
- UAT URL: https://rc-epay.esewa.com.np/api/epay/main/v2/form
- Status check URL: https://rc-epay.esewa.com.np/api/epay/transaction/status
Signature generation in Node.js
Build signed payment form data
const crypto = require('crypto');
function buildEsewaPayment({ totalAmount, transactionUuid, productCode, secretKey }) {
const signedFields = 'total_amount,transaction_uuid,product_code';
const message = `total_amount=${totalAmount},transaction_uuid=${transactionUuid},product_code=${productCode}`;
const signature = crypto.createHmac('sha256', secretKey).update(message).digest('base64');
return {
amount: totalAmount,
tax_amount: 0,
total_amount: totalAmount,
transaction_uuid: transactionUuid,
product_code: productCode,
product_service_charge: 0,
product_delivery_charge: 0,
success_url: 'https://yoursite.com/payment/success',
failure_url: 'https://yoursite.com/payment/failure',
signed_field_names: signedFields,
signature
};
}
Verify callback signature
function verifyEsewaCallback(dataBase64, secretKey) {
const json = JSON.parse(Buffer.from(dataBase64, 'base64').toString());
const message = `total_amount=${json.total_amount},transaction_uuid=${json.transaction_uuid},product_code=${json.product_code}`;
const expected = crypto.createHmac('sha256', secretKey).update(message).digest('base64');
if (expected !== json.signature) throw new Error('eSewa signature mismatch');
return json;
}
Python (Django / FastAPI)
Build payment + verify callback
import base64, hmac, hashlib, json
def build_esewa_payment(total_amount, transaction_uuid, product_code, secret_key):
message = f"total_amount={total_amount},transaction_uuid={transaction_uuid},product_code={product_code}"
signature = base64.b64encode(
hmac.new(secret_key.encode(), message.encode(), hashlib.sha256).digest()
).decode()
return {
"amount": total_amount, "tax_amount": 0, "total_amount": total_amount,
"transaction_uuid": transaction_uuid, "product_code": product_code,
"product_service_charge": 0, "product_delivery_charge": 0,
"success_url": "https://yoursite.com/payment/success",
"failure_url": "https://yoursite.com/payment/failure",
"signed_field_names": "total_amount,transaction_uuid,product_code",
"signature": signature,
}
def verify_esewa_callback(data_b64, secret_key):
payload = json.loads(base64.b64decode(data_b64))
message = f"total_amount={payload['total_amount']},transaction_uuid={payload['transaction_uuid']},product_code={payload['product_code']}"
expected = base64.b64encode(
hmac.new(secret_key.encode(), message.encode(), hashlib.sha256).digest()
).decode()
if expected != payload["signature"]:
raise ValueError("eSewa signature mismatch")
return payload
PHP
Build payment
<?php
function buildEsewaPayment($totalAmount, $transactionUuid, $productCode, $secretKey) {
$message = "total_amount=$totalAmount,transaction_uuid=$transactionUuid,product_code=$productCode";
$signature = base64_encode(hash_hmac('sha256', $message, $secretKey, true));
return [
'amount' => $totalAmount, 'tax_amount' => 0, 'total_amount' => $totalAmount,
'transaction_uuid' => $transactionUuid, 'product_code' => $productCode,
'product_service_charge' => 0, 'product_delivery_charge' => 0,
'success_url' => 'https://yoursite.com/payment/success',
'failure_url' => 'https://yoursite.com/payment/failure',
'signed_field_names' => 'total_amount,transaction_uuid,product_code',
'signature' => $signature,
];
}
function verifyEsewaCallback($dataB64, $secretKey) {
$payload = json_decode(base64_decode($dataB64), true);
$message = "total_amount={$payload['total_amount']},transaction_uuid={$payload['transaction_uuid']},product_code={$payload['product_code']}";
$expected = base64_encode(hash_hmac('sha256', $message, $secretKey, true));
if ($expected !== $payload['signature']) throw new Exception('eSewa signature mismatch');
return $payload;
}
The complete checkout flow
- User clicks "Pay with eSewa" on your site
- Server creates an invoice, generates a UUID, and builds the eSewa form data
- Server returns an HTML form that auto-submits to eSewa (or constructs a POST to the eSewa URL)
- eSewa handles the payment and redirects to your success_url with a base64-encoded
dataquery parameter - Your success-url handler decodes the data, verifies the signature, calls the status API for confirmation, marks the invoice paid, and shows the customer a receipt
Common pitfalls
- Wrong canonical string: the message must be exactly
total_amount=X,transaction_uuid=Y,product_code=Z— same field order, no spaces - UAT vs production credentials mixed: easy to test against UAT but accidentally use production keys
- Forgetting to call the status API: the signed callback proves the message came from eSewa but does not prevent replay; status API is the safeguard
- Amount precision: always use exact 2-decimal NPR (1234.50 not 1234.5)
- URL encoding the base64 data: the data param may need URL-decoding before base64-decoding
Verifying transactions server-side
The biggest mistake we see in Nepali eSewa integrations: trusting the client redirect. Always verify by calling eSewa's transaction-status endpoint with the transaction ID before marking an order paid. Otherwise an attacker can replay a success URL and skip payment entirely. See our eSewa automation guide for the complete verification pattern.
- Compare the verified amount to your order's expected amount — reject mismatches
- Compare the verified merchant code to your registered code
- Log all verification calls with timestamps for IRD audit
- Set a 5-second timeout on the verify call; fall back to "pending" if eSewa is unresponsive, never auto-success
Local testing
- Sandbox credentials: request from eSewa support; not self-service in 2026
- Localhost testing: eSewa redirects require a publicly reachable success_url — use ngrok or similar tunnel for local dev
- Test cards: sandbox provides a test mobile number + OTP combination; document this in your team wiki
- Edge cases to test: user cancels mid-checkout, OTP expires, double-redirect, browser back button mid-payment
Frequently asked questions
How long does eSewa merchant onboarding take?
For existing eSewa users: 1-2 weeks once you submit business documents. For new sign-ups: 2-4 weeks. Plan ahead for production go-live.
What does eSewa charge?
Standard merchant fees (2026) are 2–2.5% per transaction; volume-discounted above NPR 5 lakh/month. Confirm with eSewa merchant support for your specific business category.
Can I use a different success_url per transaction?
Yes — set the success_url and failure_url per payment request. Useful for multi-tenant SaaS where each customer has their own return URL.
How do I handle refunds?
eSewa supports refunds via API with the original transaction_uuid. Build a separate workflow for refunds triggered from an admin panel. Refunds settle in 1-3 working days.
Does eSewa work for international cards?
eSewa is wallet-based — it works for any wallet user with funded balance, regardless of card origin. For raw international-card payments, you need a different gateway (Stripe, Wise, etc.). See our international gateways post.
Does eSewa support recurring payments?
Not as a first-class feature in 2026. For subscriptions, integrate with auto-debit via NRB-approved bank mandates instead, or charge customers manually each cycle.
What's the integration fee?
eSewa charges transaction-percentage fees (typically 1.5-2.5% depending on merchant tier) — no integration / setup fee. Negotiated rates exist for high-volume merchants.
Who can build this in Nepal?
Praxium Labs — Nepal's AI and automation consultancy, based in Lalitpur — designs and builds the systems described in this guide for Nepali businesses and for international teams hiring from Nepal. Start a project or see all services.