After integrating Nepal payment gateways from scratch three different times, I finally decided to stop copy-pasting the same code into every project and build a proper open-source library.
That library became NepalPay Spring Boot Starter.
- 💻 GitHub: https://github.com/sujankim/nepal-pay-spring-boot-starter
- 📖 Documentation: https://sujankim.github.io/nepal-pay-spring-boot-starter/
- 🎯 JitPack: https://jitpack.io/#sujankim/nepal-pay-spring-boot-starter
How It Started
I have integrated Khalti into Java backends three different times.
The first time, I spent two full days:
- Reading documentation
- Building HTTP clients from scratch
- Manually constructing JSON payloads
- Debugging signature mismatches
- Wondering why payments showed Completed but orders were never actually charged
Eventually it worked.
Then I moved on.
The second time, I copied the code from the first project.
I went through exactly the same debugging cycle.
The same confusing eSewa HMAC signatures.
The same ConnectIPS RSA certificates.
The same amount conversion issues.
The third time, I stopped and thought:
Why am I solving the same problems over and over again?
That's when I decided to build NepalPay Spring Boot Starter.
What Is NepalPay?
NepalPay is an open-source Spring Boot starter that lets you integrate Nepal's major payment gateways with almost zero boilerplate.
No manual HTTP clients.
No JSON string formatting.
No signature generation code.
No copy-pasting from scattered blog posts.
You simply inject the clients:
@Service
@RequiredArgsConstructor
public class PaymentService {
private final KhaltiClient khaltiClient;
private final EsewaClient esewaClient;
private final ConnectIpsClient connectIpsClient;
private final FonepayClient fonepayClient;
}
Spring Boot auto-configures everything when it detects your credentials in application.yml.
No @EnableNepalPay.
No @Bean.
No configuration class.
Supported Gateways
| Gateway | Flow | Security |
|---|---|---|
| Khalti | API-first | Server-side lookup |
| eSewa | Form POST | HMAC-SHA256 verification |
| ConnectIPS | Form POST | RSA-SHA256 signing |
| Fonepay | URL Redirect | HMAC-SHA512 verification |
💳 Khalti — API First
Khalti follows a standard API-first payment flow:
Your Backend
↓
POST /initiate
↓
{ pidx, payment_url }
↓
Redirect user
↓
Khalti redirects back
↓
POST /lookup
↓
{ status: "Completed" }
↓
✅ Safe to mark as paid
With NepalPay:
KhaltiInitiateResponse response =
khaltiClient.initiatePayment(
KhaltiInitiateRequest.builder()
.amount(10000L)
.purchaseOrderId("ORD-001")
.purchaseOrderName("Pro Plan")
.build()
);
KhaltiLookupResponse lookup =
khaltiClient.lookupPayment(response.pidx());
if (lookup.isPaymentSuccessful()) {
// mark order as paid
}
⚠️ Never trust ?status=Completed in the redirect URL.
Always verify with lookupPayment().
💸 eSewa — Form Submission + HMAC Signatures
eSewa works completely differently.
Backend
↓
Generate HMAC signed form
↓
Frontend POSTs to eSewa
↓
User pays
↓
eSewa redirects back
↓
Decode Base64 callback
↓
Verify HMAC
↓
Call status API
↓
✅ Payment confirmed
Building the payload:
String uuid =
EsewaClient.generateTransactionUuid();
EsewaFormPayload payload =
esewaClient.buildFormPayload(
new BigDecimal("100.00"),
uuid
);
Verification:
EsewaClient.EsewaVerificationResult result =
esewaClient.verifyCallback(data);
if (result.isPaymentSuccessful()) {
// mark order as paid
}
🏦 ConnectIPS — RSA Signatures and Bank Transfers
ConnectIPS is the most complex gateway.
It requires:
- Merchant registration
-
.pfxcertificate - RSA-SHA256 signatures
- Strict field ordering
NepalPay handles all of this:
ConnectIpsFormPayload payload =
connectIpsClient.buildFormPayload(
ConnectIpsPaymentRequest.builder()
.txnId("TXN-001")
.amountNPR(100L)
.referenceId("ORD-001")
.remarks("Order payment")
.build()
);
Verification:
ConnectIpsValidateResponse response =
connectIpsClient.validateTransaction(
txnId,
referenceId,
txnAmtPaisa
);
if (response.isPaymentSuccessful()) {
// mark order as paid
}
🔵 Fonepay — HMAC-SHA512 URL Redirect
Fonepay is now supported in v0.4.0.
Backend
↓
Generate signed redirect URL
↓
Frontend redirects user
↓
User pays
↓
Fonepay redirects back
↓
Verify HMAC-SHA512 signature
↓
Check PS=success
↓
✅ Payment confirmed
Building the redirect URL:
FonepayRedirectParams params =
fonepayClient.buildRedirectParams(
FonepayPaymentRequest.builder()
.prn("FP-001")
.amount(100.0)
.remarks1("Pro Plan")
.build()
);
Verification:
FonepayClient.FonepayVerificationResult result =
fonepayClient.verifyCallback(callback);
if (result.isPaymentSuccessful()) {
// mark order as paid
}
The Amount Confusion Problem
Every gateway uses different amount units.
| Gateway | Unit | Java Type |
|---|---|---|
| Khalti | Paisa | long |
| eSewa | NPR | BigDecimal |
| ConnectIPS | Paisa |
amountNPR() auto-converts |
| Fonepay | NPR | double |
NepalPay makes these differences explicit to prevent silent bugs.
Supporting Spring Boot 3 and Spring Boot 4
One challenge was supporting both Spring Boot versions.
Spring Boot 3 uses:
import com.fasterxml.jackson.databind.ObjectMapper;
Spring Boot 4 uses:
import tools.jackson.databind.json.JsonMapper;
Because of this, NepalPay uses a multi-module architecture:
nepal-pay-core/
├── models
├── exceptions
└── enums
nepal-pay-spring-boot-3-starter/
└── Jackson 2
nepal-pay-spring-boot-4-starter/
└── Jackson 3
This allows the same APIs to work seamlessly across both Spring Boot generations.
Installation
Spring Boot 3.2+
Maven
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependency>
<groupId>com.github.sujankim.nepal-pay-spring-boot-starter</groupId>
<artifactId>nepal-pay-spring-boot-3-starter</artifactId>
<version>v0.4.0</version>
</dependency>
Spring Boot 4.x
Maven
<dependency>
<groupId>com.github.sujankim.nepal-pay-spring-boot-starter</groupId>
<artifactId>nepal-pay-spring-boot-4-starter</artifactId>
<version>v0.4.0</version>
</dependency>
Configuration
nepalpay:
khalti:
secret-key: ${KHALTI_SECRET_KEY}
return-url: ${KHALTI_RETURN_URL}
website-url: ${YOUR_WEBSITE_URL}
sandbox: true
esewa:
secret-key: ${ESEWA_SECRET_KEY}
product-code: ${ESEWA_PRODUCT_CODE}
success-url: ${ESEWA_SUCCESS_URL}
failure-url: ${ESEWA_FAILURE_URL}
sandbox: true
fonepay:
merchant-code: ${FONEPAY_MERCHANT_CODE}
secret-key: ${FONEPAY_SECRET_KEY}
return-url: ${FONEPAY_RETURN_URL}
sandbox: true
That's it.
Spring Boot auto-configures all client beans.
Tech Stack
- Java 17+
- Spring Boot 3.2+
- Spring Boot 4.x
- Jackson 2 & 3
- SLF4J
- MockWebServer
- Java Records
- HexFormat
The library currently provides:
KhaltiClientEsewaClientConnectIpsClientFonepayClient- 80+ tests
- Documentation website
- Consumer demo application
What I Learned Building This
1. Security Matters
Redirect URLs should never be trusted.
Server-side verification and signature validation are essential.
2. Every Gateway Is Different
- Khalti → API key
- eSewa → HMAC-SHA256
- ConnectIPS → RSA-SHA256
- Fonepay → HMAC-SHA512
Abstraction removes this complexity from application developers.
3. Multi-Module Maven Was Worth It
Supporting Spring Boot 3 and 4 in one JAR became unnecessarily difficult.
Separate starters made dependencies cleaner and testing easier.
4. Nepal Needs More Open Source Tooling
There are excellent libraries for Stripe and PayPal.
There was little in the Java ecosystem for Nepal's payment gateways.
That gap felt worth filling.
What's Next
| Feature | Status |
|---|---|
| Khalti | ✅ v0.1.0 |
| eSewa | ✅ v0.1.0 |
| ConnectIPS | ✅ v0.2.0 |
| Spring Boot 3 Support | ✅ v0.3.0 |
| Fonepay | ✅ v0.4.0 |
| Khalti Refund API | 🔲 Planned |
| Retry with Backoff | 🔲 Planned |
| Maven Central | 🔲 Planned |
Try NepalPay
📖 Documentation
https://sujankim.github.io/nepal-pay-spring-boot-starter/
💻 GitHub
https://github.com/sujankim/nepal-pay-spring-boot-starter
🎯 JitPack
https://jitpack.io/#sujankim/nepal-pay-spring-boot-starter
If NepalPay saves you time, a ⭐ on GitHub goes a long way.
Questions? Open a Discussion.
Found a bug? Open an Issue.
Want to contribute? Open a PR.
Built with ❤️ for Nepal's developer community 🇳🇵
Top comments (0)