Skip to content
Payments⏱️ 15 min read·

How to Integrate eSewa Payment Gateway in Node.js and Laravel (Official v2 ePay API)

A complete production-ready guide to integrating Nepal's eSewa payment gateway. Learn HMAC-SHA256 signature generation, sandbox testing, callbacks, and server-side transaction verification.

Need this wired into a product? See payment gateway integration.

MN
Manoj NeupaneSoftware Developer & Programming Instructor
#eSewa#Payments#Laravel#Node.js#Express#Nepal
🎯 Who This Tutorial Is For

Web developers and students in Nepal building ecommerce sites, event booking, or SaaS apps that need to accept digital wallet payments securely.

📋 Prerequisites
  • ✓Basic understanding of REST APIs and HTTP redirects
  • ✓Node.js (v18+) or PHP 8.1+ with Laravel 10/11
  • ✓Familiarity with environment variables (.env)

🚀 What You Will Learn & Build

  • ▸How eSewa's v2 ePay payment lifecycle works
  • ▸Generating the HMAC-SHA256 signature required by eSewa
  • ▸Constructing and submitting the checkout payment request
  • ▸Safely parsing the base64-encoded response payload
  • ▸Server-side verification with eSewa's Status Check API
  • ▸Preventing duplicate order processing and handling retries
1

1. Understanding eSewa's Payment Lifecycle

eSewa is Nepal's pioneer digital payment wallet. In late 2023, eSewa updated its protocol to the **v2 ePay API**, replacing the older form-based `epay_payment.action` with an **HMAC-SHA256 signed form submission**. The payment process follows these critical stages: 1. **User Checkout:** The user chooses to pay Rs. 100 on your website. Your server generates a unique `transaction_uuid` and signs the payload using HMAC-SHA256 with your merchant secret key. 2. **Redirect to eSewa:** The user is redirected to eSewa's hosted payment gateway with the signed payload. 3. **Authentication:** The user logs into eSewa via MPIN / OTP and confirms the payment. 4. **Browser Return:** eSewa redirects the user back to your `success_url` with a base64-encoded query string containing transaction details. 5. **Server-Side Verification (Crucial):** Never trust the browser redirect alone! Your backend calls eSewa's server-to-server transaction status API to confirm the funds actually arrived before delivering any product.
⚠️ Warning— Never Skip Server Verification

A malicious user can manually craft a redirect to your success URL. You MUST verify the transaction with eSewa's status API from your server before updating order status.

2

2. Test Credentials and Environment Configuration

eSewa provides a public sandbox merchant for developers. You do not need an approved business account to begin developing and testing locally. Official Test Credentials: - **Product Code:** `EPAYTEST` - **Secret Key:** `8gBm/:&EnhH.1/q` - **Sandbox Action URL:** `https://rc-epay.esewa.com.np/api/epay/main/v2/form` - **Sandbox Status Verification:** `https://rc-epay.esewa.com.np/api/epay/transaction/status/`
.env
# eSewa Sandbox Credentials
ESEWA_PRODUCT_CODE=EPAYTEST
ESEWA_SECRET_KEY=8gBm/:&EnhH.1/q
ESEWA_GATEWAY_URL=https://rc-epay.esewa.com.np/api/epay/main/v2/form
ESEWA_STATUS_CHECK_URL=https://rc-epay.esewa.com.np/api/epay/transaction/status/
APP_URL=http://localhost:3000
3

3. Generating the HMAC-SHA256 Signature

eSewa requires an HMAC-SHA256 signature calculated from a comma-separated key-value string: `total_amount=100,transaction_uuid=11-201-20,product_code=EPAYTEST` Notice that the fields must be in the exact order declared in `signed_field_names`. Any deviation or whitespace discrepancy will cause eSewa to reject the request with a **"Signature Mismatch"** error.
const crypto = require('crypto');

function generateEsewaSignature(secretKey, totalAmount, transactionUuid, productCode) {
  // Format must strictly match: total_amount=X,transaction_uuid=Y,product_code=Z
  const message = `total_amount=${totalAmount},transaction_uuid=${transactionUuid},product_code=${productCode}`;
  
  const hash = crypto
    .createHmac('sha256', secretKey)
    .update(message)
    .digest('base64');
    
  return hash;
}

module.exports = { generateEsewaSignature };
4

4. Creating the Payment Initiation Endpoint

When the customer clicks "Pay with eSewa", your server calculates the order amount, stores a pending order record with a unique UUID, generates the signature, and sends the form parameters to the client (or renders an auto-submitting form).
const express = require('express');
const router = express.Router();
const { v4: uuidv4 } = require('uuid');
const { generateEsewaSignature } = require('../esewaSignature');

router.post('/initiate-esewa', async (req, res) => {
  const { amount, productId } = req.body;
  const totalAmount = Number(amount).toFixed(2);
  const transactionUuid = `ORD-${Date.now()}-${uuidv4().substring(0, 8)}`;
  const productCode = process.env.ESEWA_PRODUCT_CODE;
  const secretKey = process.env.ESEWA_SECRET_KEY;

  const signature = generateEsewaSignature(secretKey, totalAmount, transactionUuid, productCode);

  const esewaPayload = {
    amount: totalAmount,
    tax_amount: "0",
    total_amount: totalAmount,
    transaction_uuid: transactionUuid,
    product_code: productCode,
    product_service_charge: "0",
    product_delivery_charge: "0",
    success_url: `${process.env.APP_URL}/api/payment/esewa/success`,
    failure_url: `${process.env.APP_URL}/api/payment/esewa/failure`,
    signed_field_names: "total_amount,transaction_uuid,product_code",
    signature: signature
  };

  // Return payload to frontend to submit to eSewa
  res.json({
    gatewayUrl: process.env.ESEWA_GATEWAY_URL,
    params: esewaPayload
  });
});

module.exports = router;
5

5. Handling Success Callback & Server-Side Verification

Upon successful payment, eSewa redirects to your `success_url` with a query parameter named `data`. This string is base64-encoded JSON containing: ```json { "transaction_code": "0008544", "status": "COMPLETE", "total_amount": "100.00", "transaction_uuid": "ORD-1715420101-abc12", "product_code": "EPAYTEST", "signature": "..." } ``` You must parse this data, verify the transaction status directly with eSewa's HTTP endpoint, check that the amount matches your database record, and then fulfill the order.
const axios = require('axios');

router.get('/esewa/success', async (req, res) => {
  try {
    const { data } = req.query;
    if (!data) return res.status(400).send('Missing eSewa callback payload');

    // Decode base64 payload
    const decodedBuffer = Buffer.from(data, 'base64');
    const callbackData = JSON.parse(decodedBuffer.toString('utf-8'));

    const { total_amount, transaction_uuid, product_code } = callbackData;

    // Verify with eSewa Server-to-Server Status API
    const verifyUrl = `${process.env.ESEWA_STATUS_CHECK_URL}?product_code=${product_code}&total_amount=${total_amount}&transaction_uuid=${transaction_uuid}`;
    
    const verifyRes = await axios.get(verifyUrl);

    if (verifyRes.data.status === 'COMPLETE') {
      // 1. Check if order was already marked completed (Idempotency)
      // 2. Verify total_amount matches the amount in your database
      // 3. Update order status to PAID
      console.log('Payment verified successfully:', verifyRes.data);
      return res.redirect(`/order-success?uuid=${transaction_uuid}`);
    } else {
      return res.redirect('/order-failed?reason=verification_failed');
    }
  } catch (err) {
    console.error('eSewa verification error:', err.message);
    return res.status(500).send('Transaction verification failed');
  }
});

✨ Expected Output & Verification

When the test transaction is complete: - eSewa redirects to: http://localhost:3000/api/payment/esewa/success?data=eyJ0cmFuc2FjdGlvbl9jb2Rl... - Server verifies with https://rc-epay.esewa.com.np/api/epay/transaction/status/ - Response returns HTTP 200 with JSON: { "product_code": "EPAYTEST", "total_amount": 100.0, "status": "COMPLETE", "ref_id": "0008544", "transaction_uuid": "ORD-1715420101-abc12" }

🧪 How to Test the Implementation

  1. 1Use test eSewa ID: 9806800001 / 9806800002 / 9806800003
  2. 2Password/MPIN: 1122
  3. 3OTP Code sent in sandbox: 123456
  4. 4Confirm amount deduction in test wallet and redirect to success URL
  5. 5Inspect server terminal logs to ensure HTTP GET status check returns COMPLETE

⚠️ Common Errors & How to Fix Them

❌Signature Mismatch / Invalid Signature

Root Cause: Field ordering in string is incorrect, or amount formatted with differing decimals (e.g. 100 vs 100.00).

Fix: Format amount strictly to 2 decimal places and verify field sequence: total_amount,transaction_uuid,product_code.

❌Transaction Not Found in Status Check

Root Cause: Querying status check URL before the customer finished entering MPIN, or using mismatched transaction_uuid.

Fix: Ensure transaction_uuid is stored in session/DB and exactly matches the value returned in the decoded data query parameter.

❌CORS error when redirecting to eSewa

Root Cause: Attempting to perform fetch() or axios.post() directly to eSewa form URL from JavaScript frontend.

Fix: eSewa v2 requires a standard browser navigation / form POST submission, not an AJAX request.

🛡️ Security Considerations & Best Practices

  • !Never store the ESEWA_SECRET_KEY in frontend React or Next.js client bundles.
  • !Always verify the total_amount in callback matches the original database order amount (avoids tampering).
  • !Implement database locking (e.g. SELECT FOR UPDATE in MySQL) to avoid double crediting if callbacks fire multiple times.

❓ Frequently Asked Questions (FAQ)

Sign a merchant agreement with eSewa at merchant.esewa.com.np. They will issue your live Product Code and Secret Key. In your production .env, update ESEWA_PRODUCT_CODE, ESEWA_SECRET_KEY, and change the URLs from rc-epay to epay.esewa.com.np.

📌 Summary of Key Takeaways

  • ✓eSewa v2 uses HMAC-SHA256 signatures with base64 encoding.
  • ✓Required signature format: total_amount=X,transaction_uuid=Y,product_code=Z.
  • ✓Always verify transaction status server-side using eSewa status check API.
  • ✓Store merchant secrets securely in server environment variables.

Official References & Further Reading