0%
PRAXIUM LABS

Namaste! 🇳🇵

You found our hidden gem! Something incredible is brewing in the heart of the Himalayas. We might have something special here for you soon.

Stay curious. Jay Nepal!

Share

eSewa Merchant Integration: Code Walkthrough for Node.js, Python, PHP (2026)

eSewa Merchant Integration: Code Walkthrough for Node.js, Python, PHP (2026)

TL;DR. Working eSewa ePay v2 integration is three things: (1) build a signed payment-initiation form with HMAC-SHA256, (2) handle the base64-encoded callback and verify the signature, (3) confirm via eSewa's status API for defence in depth. Below: working code for Node.js, Python (Django/FastAPI), and PHP. UAT credentials shown — swap for production keys when going live.

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 data query 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.