How eSewa Works
eSewa uses a form POST model โ unlike Khalti which is API-first. Your backend generates a signed payload, returns it to the frontend, and the frontend submits a form directly to eSewa. The backend never makes a direct HTTP call to eSewa during initiation.
Configure
nepalpay:
esewa:
secret-key: ${ESEWA_SECRET_KEY}
product-code: ${ESEWA_PRODUCT_CODE}
success-url: ${ESEWA_SUCCESS_URL}
failure-url: ${ESEWA_FAILURE_URL}
sandbox: true # false for production
# Optional retry (applies to checkStatus() inside verifyCallback)
retry:
enabled: true
max-attempts: 3
initial-delay-ms: 500
multiplier: 2.0
max-delay-ms: 5000
| Property | Required | Default | Description |
|---|---|---|---|
secret-key |
โ | โ | HMAC-SHA256 secret key. Sandbox: 8gBm/:&EnhH.1/q |
product-code |
โ | โ | Sandbox: EPAYTEST ยท Production: your merchant code |
success-url |
โ | โ | eSewa redirects here on successful payment with ?data=BASE64 |
failure-url |
โ | โ | eSewa redirects here on failed or canceled payment |
sandbox |
โ | true |
true = rc-epay.esewa.com.np ยท false = epay.esewa.com.np |
timeout-seconds |
โ | 10 |
HTTP timeout for status API calls in seconds |
buildFormPayload()
Simple overload โ amount only
// Generate a unique UUID for this transaction
String uuid = EsewaClient.generateTransactionUuid();
// โ
Save uuid to DB BEFORE returning payload to frontend
orderRepo.saveUuid(orderId, uuid);
// eSewa uses NPR directly โ NOT paisa like Khalti
EsewaFormPayload payload =
esewaClient.buildFormPayload(
new BigDecimal("100.00"), // NPR 100
uuid
);
// Return payload to frontend โ they will POST it
Full overload โ with charges
EsewaFormPayload payload =
esewaClient.buildFormPayload(
new BigDecimal("100.00"), // base amount NPR
new BigDecimal("10.00"), // tax NPR (null = zero)
uuid,
new BigDecimal("5.00"), // service charge NPR (null = zero)
new BigDecimal("0.00") // delivery charge NPR (null = zero)
);
// totalAmount = 100 + 10 + 5 + 0 = 115.00
eSewa uses NPR โ not paisa.
NPR 100 โ send new BigDecimal("100.00").
Charge components (tax, service, delivery) cannot
be negative โ NepalPay validates this and throws
EsewaException if they are.
EsewaFormPayload fields
| Field | JSON key | Description |
|---|---|---|
amount() |
amount |
Base amount as String e.g. "100.00" |
taxAmount() |
tax_amount |
Tax as String |
totalAmount() |
total_amount |
Sum of all charges as String |
transactionUuid() |
transaction_uuid |
Your unique transaction ID |
productCode() |
product_code |
Your eSewa merchant code |
signature() |
signature |
HMAC-SHA256 Base64 signature โ generated by NepalPay |
signedFieldNames() |
signed_field_names |
Always: total_amount,transaction_uuid,product_code |
formActionUrl() |
form_action_url |
The eSewa form URL to POST to (sandbox or production) |
Frontend Form Submission
Your frontend must POST all payload fields as a
form submission to payload.formActionUrl().
// Angular / TypeScript example
submitEsewaPayment(payload: EsewaFormPayload): void {
const form = document.createElement('form');
form.method = 'POST';
form.action = payload.form_action_url;
const fields = {
amount: payload.amount,
tax_amount: payload.tax_amount,
total_amount: payload.total_amount,
transaction_uuid: payload.transaction_uuid,
product_code: payload.product_code,
product_service_charge: payload.product_service_charge,
product_delivery_charge:payload.product_delivery_charge,
success_url: payload.success_url,
failure_url: payload.failure_url,
signed_field_names: payload.signed_field_names,
signature: payload.signature
};
Object.entries(fields).forEach(([key, value]) => {
const input = document.createElement('input');
input.type = 'hidden';
input.name = key;
input.value = value ?? '';
form.appendChild(input);
});
document.body.appendChild(form);
form.submit();
}
The signature is generated entirely
on your backend using your HMAC-SHA256 secret key.
The HMAC secret never reaches the frontend.
Only the resulting signature value is sent
as a form field.
verifyCallback()
eSewa redirects to your success-url with
a ?data=BASE64_ENCODED_JSON query parameter.
verifyCallback() handles all three steps
automatically.
@GetMapping("/esewa/callback")
public ResponseEntity<String> callback(
@RequestParam String data) {
// verifyCallback does all three steps:
// 1. Decode Base64 โ JSON
// 2. Verify HMAC-SHA256 signature (constant-time)
// 3. Call eSewa status API
EsewaClient.EsewaVerificationResult result =
esewaClient.verifyCallback(data);
if (!result.isPaymentSuccessful()) {
return ResponseEntity.badRequest()
.body("Payment not confirmed: " +
result.statusResponse().status());
}
// Load order using saved uuid
String uuid = result.callbackData().transactionUuid();
orderRepo.markPaid(uuid);
return ResponseEntity.ok("Payment confirmed");
}
// Handle failure redirect
@GetMapping("/esewa/failed")
public ResponseEntity<String> failed() {
return ResponseEntity.ok("Payment was canceled or failed");
}
If the HMAC signature does not match,
verifyCallback() throws
EsewaException with the message
"signature verification FAILED".
This increments the
nepalpay.esewa.callback.signature.failed
Micrometer counter โ alert on a spike.
EsewaVerificationResult fields
| Method | Returns | Description |
|---|---|---|
isPaymentSuccessful() |
boolean |
true only when HMAC matched AND status is COMPLETE |
verified() |
boolean |
Same as isPaymentSuccessful() |
callbackData() |
EsewaCallbackData |
Decoded callback JSON from eSewa |
statusResponse() |
EsewaStatusResponse |
Response from the eSewa status API |
checkStatus()
Call the eSewa status API directly without going through the callback flow. Useful for:
- Manually checking a payment status
- Background reconciliation jobs
- Handling cases where the callback was missed
// uuid = the transaction_uuid you saved during initiation
// totalAmount = the exact total amount string used during initiation
EsewaStatusResponse status =
esewaClient.checkStatus(uuid, "100.00");
if (status.isPaymentSuccessful()) {
// status.status() == "COMPLETE"
orderRepo.markPaid(uuid);
}
eSewa status is COMPLETE
โ not COMPLETED.
This is different from Khalti which uses
Completed.
NepalPay handles both correctly โ just never compare
the raw status strings directly in your code.
Always use isPaymentSuccessful().
Security Rules
Never trust the
?data=BASE64 redirect parameter
alone. The Base64 data can be forged.
Always call verifyCallback(data)
which re-computes the HMAC-SHA256 signature
and calls the status API.
NepalPay uses constant-time comparison
(MessageDigest.isEqual()) in both
blocking and reactive eSewa clients โ
preventing timing attacks on the HMAC signature.
Always save uuid to your database
before returning the payload to the frontend.
Use the saved uuid โ not the one from the callback โ
when calling checkStatus().
# Correct eSewa flow โ all steps required:
1. generateTransactionUuid() โ save to DB
2. buildFormPayload() โ return to frontend
3. Frontend POSTs to eSewa
4. eSewa redirects ?data=BASE64
5. verifyCallback(data) โ all 3 sub-steps automatic
6. isPaymentSuccessful() โ then mark paid
Reactive Usage
Use EsewaReactiveClient from the reactive
starter. buildFormPayload() is synchronous
(no HTTP). verifyCallback() and
checkStatus() return Mono.
Build payload โ synchronous
// Synchronous โ no Mono needed
public EsewaFormPayload startEsewa(
String orderId, BigDecimal amount) {
String uuid = EsewaReactiveClient.generateTransactionUuid();
orderRepo.saveUuid(orderId, uuid); // save first!
return esewaReactiveClient.buildFormPayload(amount, uuid);
}
Verify callback โ reactive
public Mono<Boolean> verifyEsewa(String encodedData) {
return esewaReactiveClient.verifyCallback(encodedData)
.map(EsewaReactiveClient.EsewaVerificationResult
::isPaymentSuccessful);
}
Check status directly โ reactive
public Mono<Boolean> checkEsewaStatus(
String uuid, String totalAmount) {
return esewaReactiveClient.checkStatus(uuid, totalAmount)
.map(EsewaStatusResponse::isPaymentSuccessful);
}
The reactive client uses
Mono.fromCallable() for the
synchronous HMAC and signature verification steps,
so all exceptions are emitted as reactive error
signals โ never thrown outside the pipeline.
eSewa Status Values
| Status string | isPaymentSuccessful() | Meaning |
|---|---|---|
COMPLETE |
โ true | Payment fully completed โ safe to fulfil order |
INCOMPLETE |
false | Payment was not completed |
| anything else | false | Unknown status โ do not mark as paid |
eSewa uses COMPLETE
(not COMPLETED). Never compare
raw status strings in your code.
Always use isPaymentSuccessful().
Sandbox Credentials
| Field | Sandbox Value |
|---|---|
| eSewa ID | 9806800001 |
| Password | Nepal@123 |
| MPIN | 1122 |
| Token | 123456 |
Secret Key (secret-key) |
8gBm/:&EnhH.1/q |
Product Code (product-code) |
EPAYTEST |
| Form Action URL | https://rc-epay.esewa.com.np/api/epay/main/v2/form |
| Status API | https://rc.esewa.com.np/api/epay/transaction/status/ |
Never hardcode sandbox credentials. Even though these are public sandbox values, always use environment variables so you have a consistent pattern when switching to production.