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.