The first production-grade Java starter for Khalti, eSewa, ConnectIPS, and Fonepay. Blocking, reactive, retry, metrics, and security — trusted by Nepal's developer community.
<dependency>
<groupId>io.github.sujankim</groupId>
<artifactId>nepal-pay-spring-boot-3-starter</artifactId>
<version>1.2.0</version>
</dependency>
<dependency>
<groupId>io.github.sujankim</groupId>
<artifactId>nepal-pay-spring-boot-4-starter</artifactId>
<version>1.2.0</version>
</dependency>
<dependency>
<groupId>io.github.sujankim</groupId>
<artifactId>nepal-pay-spring-boot-reactive-starter</artifactId>
<version>1.2.0</version>
</dependency>
implementation 'io.github.sujankim:nepal-pay-spring-boot-3-starter:1.2.1'
implementation 'io.github.sujankim:nepal-pay-spring-boot-4-starter:1.2.1'
implementation 'io.github.sujankim:nepal-pay-spring-boot-reactive-starter:1.2.1'
implementation("io.github.sujankim:nepal-pay-spring-boot-3-starter:1.2.1")
implementation("io.github.sujankim:nepal-pay-spring-boot-4-starter:1.2.1")
implementation("io.github.sujankim:nepal-pay-spring-boot-reactive-starter:1.2.1")
All starters share the same
nepalpay.*
YAML — same config, switch artifact to migrate.
·
Full install guide →
After integrating Nepal payment gateways from scratch three different times — the same HMAC debugging, the same RSA certificate handling — NepalPay was built to solve this once and for all.
Add dependency → configure keys → inject and use.
No @Bean, no config class.
Auto-configured by Spring Boot.
Spring MVC or WebFlux — same YAML,
same API pattern. Reactive returns
Mono<T> via WebClient.
HMAC-SHA256, HMAC-SHA512, RSA-SHA256, constant-time comparison, server-side verification — all built in.
Exponential backoff with jitter. Never retries 4xx or signature failures. Opt-in, configurable per gateway.
Operation timers, per-operation retry counters, HMAC failure alerts. Auto-registered when Actuator is on the classpath.
Every configured gateway under
/actuator/health.
Config-only health check —
no outbound pings.
One library. Consistent API across Khalti, eSewa, ConnectIPS, and Fonepay — blocking and reactive.
API-first initiation, server-side lookup, full and partial refund support.
Explore docs →HMAC-SHA256 signed form payload, Base64 callback decode and signature verification.
Explore docs →RSA-SHA256 signing via CREDITOR.pfx. Bank-to-bank via NCHL. Configurable timeout.
Explore docs →HMAC-SHA512 signed URL redirect for QR, mobile banking, and wallet payments.
Explore docs →Khalti payment initiation — the honest comparison.
HttpClient client = HttpClient.newHttpClient();
String json = """
{
"return_url": "%s",
"website_url": "%s",
"amount": %d,
"purchase_order_id": "%s",
"purchase_order_name": "%s"
}
""".formatted(returnUrl, url, amount, id, name);
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(
"https://dev.khalti.com/api/v2/epayment/initiate/"))
.header("Authorization", "Key " + secretKey)
.header("Content-Type", "application/json")
.POST(BodyPublishers.ofString(json))
.build();
// parse manually, handle errors manually,
// write HMAC for eSewa manually,
// repeat for every new project...
// Zero @Bean. Zero @EnableNepalPay.
@Service
@RequiredArgsConstructor
public class PaymentService {
private final KhaltiClient khaltiClient;
public String initiate(
String orderId, long nprAmount) {
var res = khaltiClient.initiatePayment(
KhaltiInitiateRequest.builder()
.amount(nprAmount * 100L)
.purchaseOrderId(orderId)
.purchaseOrderName("Pro Plan")
.build()
);
// Save pidx to DB, then redirect:
return res.paymentUrl();
}
}
All share the same nepalpay.* YAML.
Switch the artifact name to migrate.
Only the HTTP client and return type differ.
RestClient-based. Use with Spring MVC. Jackson 2 internally. Java 17+.
nepal-pay-spring-boot-3-starter
Same API as Boot 3. Jackson 3 internally. Required for Spring Boot 4.x.
nepal-pay-spring-boot-4-starter
WebClient-based. Returns
Mono<T>.
Use with Spring WebFlux.
nepal-pay-spring-boot-reactive-starter
Add spring-boot-starter-actuator — NepalPay
automatically instruments every gateway and registers
a health indicator per configured gateway.
Every HTTP call timed and tagged by
gateway, operation,
status. P99 in Grafana.
Initiate, lookup, and refund each get their own retry counter — not a shared one.
nepalpay.esewa.callback
— spike means tampering.
.signature.failed
Each gateway under
/actuator/health.
Config-only — no outbound pings.
Timer.Sample +
doOnSuccess/doOnError.
Fully non-blocking throughout.
nepalpay.metrics.enabled=false
or
nepalpay.health.enabled=false.
No Actuator = nothing registers.
# Khalti P99 initiation latency
histogram_quantile(0.99,
rate(nepalpay_khalti_payment_initiate_duration_seconds_bucket[5m])
)
# eSewa fraud alert — HMAC mismatch spike
rate(nepalpay_esewa_callback_signature_failed_total[5m]) > 5
Disabled by default. Enable for any HTTP-backed gateway with four YAML lines.
nepalpay:
khalti:
retry:
enabled: true
max-attempts: 3
initial-delay-ms: 500
multiplier: 2.0
max-delay-ms: 5000
| Gateway | Retry Applies To | Retry? |
|---|---|---|
| Khalti |
initiatePayment()
lookupPayment()
refundPayment()
|
✅ Yes |
| eSewa | checkStatus() inside
verifyCallback() |
✅ Yes |
| ConnectIPS | validateTransaction() |
✅ Yes |
| Fonepay | URL redirect — no HTTP calls | ❌ N/A |
Retries: 5xx + network timeouts ·
Never: 4xx + signature failures ·
Refund safety: verify with
lookupPayment() before retrying
to avoid double-refunds.
The most common source of integration bugs. Each gateway expects a different Java type and amount unit.
Gateway redirect URL parameters can be faked by anyone. Always verify server-side.
Secret keys never leave your backend.
lookupPayment() and
verifyCallback() enforce
server-side confirmation.
eSewa and Fonepay signatures verified before marking payment successful. Constant-time comparison — timing-safe.
isAmountValid(expectedPaisa)
on Khalti lookup detects tampered amounts.
Always call after success.
Never trust redirect URL parameters alone —
?status=Completed,
?PS=success,
?data=BASE64
can all be faked.
Never put secret keys in frontend code or hardcode them in source. Use environment variables for all credentials.
Never commit CREDITOR.pfx to Git.
Add *.pfx to
.gitignore before your first commit.