Requirements
Merchant registration required before use. ConnectIPS requires registration with NCHL. You cannot test without NCHL credentials. Contact connectips@nchl.com.np or your bank.
From NCHL you will receive:
- Merchant ID (integer)
- Application ID and Application Name
- Application Password (for HTTP Basic Auth)
CREDITOR.pfxcertificate file- Password for the
.pfxfile
Never commit CREDITOR.pfx to Git.
Add *.pfx to your
.gitignore immediately.
The .pfx contains your RSA private key.
Anyone with it can forge ConnectIPS payment tokens.
How ConnectIPS Works
The RSA private key is cached at startup
from your CREDITOR.pfx file.
NepalPay iterates all KeyStore aliases and selects
the first isKeyEntry() โ safe for
multi-entry PFX files.
If the .pfx is invalid or missing,
startup fails immediately with a clear error โ
never silently at payment time.
Configure
nepalpay:
connectips:
merchant-id: ${CONNECTIPS_MERCHANT_ID} # e.g. 550
app-id: ${CONNECTIPS_APP_ID} # e.g. "MER-550-APP-1"
app-name: ${CONNECTIPS_APP_NAME}
app-password: ${CONNECTIPS_APP_PASSWORD}
pfx-path: ${CONNECTIPS_PFX_PATH} # file:/app/CREDITOR.pfx
pfx-password: ${CONNECTIPS_PFX_PASSWORD}
timeout-seconds: 30 # default 30s โ increase for slow banks
sandbox: true # false for production
# Optional retry
retry:
enabled: true
max-attempts: 3
initial-delay-ms: 500
multiplier: 2.0
max-delay-ms: 5000
| Property | Required | Default | Description |
|---|---|---|---|
merchant-id |
โ | โ | NCHL integer merchant ID. Bean created when present. |
app-id |
โ | โ | Application ID from NCHL. |
app-name |
โ | โ | Application name from NCHL. |
app-password |
โ | โ | Used for HTTP Basic Auth on the validate API. |
pfx-path |
โ | โ | Spring Resource path to CREDITOR.pfx. See formats below. |
pfx-password |
โ | โ | Password for the CREDITOR.pfx file. |
sandbox |
โ | true |
true = uat.connectips.com ยท false = connectips.com |
timeout-seconds |
โ | 30 |
HTTP timeout in seconds. Default 30s โ intentionally longer than other gateways because ConnectIPS validates via NCHL to bank systems which can be slower. Increase if you see timeouts. |
pfx-path formats
| Format | Example | Use case |
|---|---|---|
file: |
file:/app/CREDITOR.pfx |
Absolute path on server โ recommended for production |
classpath: |
classpath:CREDITOR.pfx |
Inside JAR โ not recommended for production |
buildFormPayload()
// Generate a unique transaction ID
String txnId = "TXN-" + orderId + "-" + System.currentTimeMillis();
// โ
Save txnId to DB BEFORE returning to frontend
orderRepo.saveTxnId(orderId, txnId);
ConnectIpsFormPayload payload = connectIpsClient.buildFormPayload(
ConnectIpsPaymentRequest.builder()
.txnId(txnId)
.amountNPR(100L) // auto-converts NPR โ paisa (ร100)
.referenceId(orderId)
.remarks("Payment for order " + orderId)
.particulars("MyApp")
.build()
);
// Return payload to frontend
// Frontend POSTs UPPERCASE field names to payload.formActionUrl()
Use .amountNPR(100L) for automatic
NPR โ paisa conversion (ร100).
Or use .txnAmtPaisa(10000L) if you
already have the paisa amount.
ConnectIPS uses paisa internally โ
NPR 100 = 10000 paisa.
ConnectIpsPaymentRequest builder fields
| Method | Required | Description |
|---|---|---|
.txnId(String) |
Required | Unique transaction ID โ save to DB first |
.amountNPR(long) |
Required | Amount in NPR โ auto-converts to paisa ร100 |
.txnAmtPaisa(long) |
Required | Alternative โ amount directly in paisa |
.referenceId(String) |
Required | Your order or reference ID |
.remarks(String) |
Optional | Shown to user on ConnectIPS gateway |
.particulars(String) |
Optional | Additional particulars field |
Frontend Form Submission
ConnectIPS uses UPPERCASE field names
in the form POST.
The TOKEN field contains the RSA-SHA256
digital signature โ generated on your server using the
CREDITOR.pfx private key, which never reaches the frontend.
Only the resulting signed token value is sent as a form field.
// Angular / TypeScript example
submitConnectIps(payload: ConnectIpsFormPayload): void {
const form = document.createElement('form');
form.method = 'POST';
form.action = payload.form_action_url; // formActionUrl()
// ConnectIPS field names are UPPERCASE
const fields = {
MERCHANTID: payload.MERCHANTID, // merchantId()
APPID: payload.APPID, // appId()
APPNAME: payload.APPNAME, // appName()
TXNID: payload.TXNID, // txnId()
TXNDATE: payload.TXNDATE, // txnDate()
TXNCRNCY: payload.TXNCRNCY, // txnCrncy() โ always "NPR"
TXNAMT: payload.TXNAMT, // txnAmt() โ in paisa
REFERENCEID: payload.REFERENCEID, // referenceId()
REMARKS: payload.REMARKS, // remarks()
PARTICULARS: payload.PARTICULARS, // particulars()
TOKEN: payload.TOKEN // RSA-SHA256 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();
}
validateTransaction()
Called after ConnectIPS redirects to your callback URL. Always call this โ never trust the redirect.
@GetMapping("/connectips/callback")
public ResponseEntity<String> callback(
@RequestParam String txnId,
@RequestParam String referenceId,
@RequestParam long txnAmt) {
// โ
Always validate server-side โ never trust redirect params
ConnectIpsValidateResponse res =
connectIpsClient.validateTransaction(
txnId, referenceId, txnAmt);
if (!res.isPaymentSuccessful()) {
return ResponseEntity.badRequest()
.body("Payment not confirmed: " + res.statusDesc());
}
// โ
Load order from DB using the saved txnId โ NOT referenceId from redirect
// The redirect referenceId can be attacker-controlled.
// Use the txnId you saved during buildFormPayload() to look up the order.
String orderId = orderRepo.findOrderByTxnId(txnId);
orderRepo.markPaid(orderId);
return ResponseEntity.ok("Payment confirmed");
}
}
ConnectIpsValidateResponse fields
| Method | Returns | Description |
|---|---|---|
isPaymentSuccessful() |
boolean |
true only when status is SUCCESS |
status() |
String |
SUCCESS or FAILED |
statusDesc() |
String |
Human-readable description e.g. "TRANSACTION SUCCESSFUL" |
referenceId() |
String |
Your original order/reference ID echoed back |
txnAmt() |
String |
Amount transacted in paisa |
merchantId() |
Integer |
Your NCHL merchant ID echoed back |
paymentStatus() |
ConnectIpsPaymentStatus |
Typed enum: SUCCESS, FAILED, UNKNOWN |
Test-friendly overload
// Skips RSA signing โ useful in tests with mock token
ConnectIpsValidateResponse res =
connectIpsClient.validateTransactionWithToken(
referenceId, // your order/reference ID
txnAmtPaisa, // amount in paisa
"MOCK_TOKEN" // pre-built token
);
Security Rules
Never commit CREDITOR.pfx to Git.
Add *.pfx to .gitignore.
Load it via an environment variable path.
Never trust the callback redirect.
Always call validateTransaction()
server-side with HTTP Basic Auth.
The redirect parameters can be faked.
The RSA private key is generated entirely on your server using the CREDITOR.pfx certificate. It is cached at startup โ no KeyStore loading on every payment. Only the resulting signed TOKEN value reaches the frontend as a form field.
NepalPay iterates all KeyStore aliases and checks
isKeyEntry() before selecting โ
safe for PFX files with multiple entries
(certificate chain + private key).
# .gitignore โ add these immediately
*.pfx
CREDITOR.pfx
# application.yml โ always load from environment variable
nepalpay:
connectips:
pfx-path: ${CONNECTIPS_PFX_PATH} # file:/app/CREDITOR.pfx
pfx-password: ${CONNECTIPS_PFX_PASSWORD}
Reactive Usage
Use ConnectIpsReactiveClient from the
reactive starter. buildFormPayload()
is synchronous. validateTransaction()
returns Mono with both validation
and RSA signing wrapped inside
Mono.defer().
Build form payload โ synchronous
// Synchronous โ RSA signing, no HTTP
public ConnectIpsFormPayload startConnectIps(
String orderId, long amountNPR) {
String txnId = "TXN-" + orderId + "-"
+ System.currentTimeMillis();
orderRepo.saveTxnId(orderId, txnId); // save first!
return connectIpsReactiveClient.buildFormPayload(
ConnectIpsPaymentRequest.builder()
.txnId(txnId)
.amountNPR(amountNPR)
.referenceId(orderId)
.build()
);
}
Validate transaction โ reactive
@GetMapping("/connectips/callback")
public Mono<ResponseEntity<String>> callback(
@RequestParam String txnId,
@RequestParam String referenceId,
@RequestParam long txnAmt) {
// Both validation and RSA signing inside Mono.defer()
// All errors emitted as Mono.error() โ never thrown
return connectIpsReactiveClient
.validateTransaction(txnId, referenceId, txnAmt)
.flatMap(res -> {
if (!res.isPaymentSuccessful()) {
return Mono.just(ResponseEntity.badRequest()
.body("Not confirmed: " + res.statusDesc()));
}
return orderRepo.markPaid(referenceId)
.thenReturn(ResponseEntity.ok(
"Payment confirmed"));
});
}
The reactive client applies the configurable timeout
via ReactorClientHttpConnector +
Reactor Netty's HttpClient.responseTimeout()
โ fully non-blocking, no thread sleep.
Set nepalpay.connectips.timeout-seconds
to increase for slower bank connections.
How ConnectIPS Differs from Other Gateways
| Feature | Khalti | eSewa | ConnectIPS |
|---|---|---|---|
| Flow | API POST | Form POST | Form POST |
| Signature | API Key header | HMAC-SHA256 Base64 | RSA-SHA256 via .pfx |
| Amount unit | Paisa | NPR (BigDecimal) | Paisa (amountNPR auto-converts) |
| Verify auth | Bearer key | No auth | HTTP Basic Auth |
| Registration | Self-service | Self-service | NCHL / bank required |
| Sandbox | Self-service | Self-service | NCHL UAT only |
| HTTP timeout | 10s (default) | 10s (default) | 30s (default) โ configurable |