How eSewa Works

โ„น๏ธ eSewa uses a form-submission model โ€” unlike Khalti. Your backend builds a signed payload. Your frontend submits it as a form.
Backend: builds EsewaFormPayload (with HMAC signature)
    โ†“ return to frontend
Frontend: POSTs form directly to eSewa URL
    โ†“
User pays on eSewa
    โ†“
eSewa redirects to: yourSuccessUrl?data=BASE64_JSON
    โ†“
โš ๏ธ  DO NOT trust this redirect alone!
    โ†“
Backend: decodes Base64 โ†’ verifies HMAC โ†’ calls status API
    โ†“
โœ… mark order as paid

buildFormPayload()

// Simple: just amount and UUID
String uuid = EsewaClient.generateTransactionUuid();
orderRepo.saveUuid(orderId, uuid); // store BEFORE returning!

EsewaFormPayload payload = esewaClient.buildFormPayload(
    new BigDecimal("100.00"),  // NPR โ€” NOT paisa!
    uuid
);

// With tax and charges:
EsewaFormPayload payload = esewaClient.buildFormPayload(
    new BigDecimal("100.00"),  // amount
    new BigDecimal("13.00"),   // tax
    uuid,                      // transactionUuid
    BigDecimal.ZERO,           // serviceCharge
    BigDecimal.ZERO            // deliveryCharge
    // total = 100 + 13 = 113.00
);
โš ๏ธ eSewa uses NPR directly โ€” not paisa. NPR 100 โ†’ send new BigDecimal("100.00"). This is opposite to Khalti.

Frontend Form Submission

Your frontend must POST the payload fields to eSewa's URL.

// Angular / TypeScript:
initiateEsewa(payload: EsewaFormPayload): void {
    const form = document.createElement('form');
    form.method = 'POST';
    form.action = payload.form_action_url;

    const fields = ['amount', 'tax_amount', 'total_amount',
        'transaction_uuid', 'product_code', 'product_service_charge',
        'product_delivery_charge', 'success_url', 'failure_url',
        'signed_field_names', 'signature'];

    fields.forEach(field => {
        const input = document.createElement('input');
        input.type = 'hidden';
        input.name = field;
        input.value = (payload as any)[field];
        form.appendChild(input);
    });

    document.body.appendChild(form);
    form.submit();
}

verifyCallback()

// eSewa redirects to: yourSuccessUrl?data=BASE64_JSON
// Extract the "data" query parameter and pass it here

@GetMapping("/esewa/callback")
public ResponseEntity<String> callback(@RequestParam String data) {

    // verifyCallback() does THREE things automatically:
    // 1. Decodes Base64 โ†’ EsewaCallbackData
    // 2. Verifies HMAC-SHA256 signature (tamper protection)
    // 3. Calls eSewa status API for final confirmation

    EsewaClient.EsewaVerificationResult result =
        esewaClient.verifyCallback(data);

    if (!result.isPaymentSuccessful()) {
        return ResponseEntity.badRequest().body("Payment not confirmed");
    }

    String uuid = result.callbackData().transactionUuid();
    orderRepo.markPaid(uuid, result.statusResponse().refId());

    return ResponseEntity.ok("Payment successful");
}

checkStatus()

// Poll status directly without going through callback flow
// Useful for cron jobs checking pending orders

EsewaStatusResponse status = esewaClient.checkStatus(
    "your-transaction-uuid",  // saved from buildFormPayload()
    "100.00"                   // original total amount
);

if (status.isPaymentSuccessful()) {
    orderRepo.markPaid(uuid);
}

Security Rules

๐Ÿ”ด Never trust redirect URL parameters alone. Always call verifyCallback(data).
๐Ÿ”ด Never expose your secret key in frontend code. The HMAC signing must happen on your backend only.
โœ… Store transactionUuid in your database before returning the form payload to the frontend.
โœ… verifyCallback() automatically validates the HMAC signature โ€” a mismatch throws EsewaException.