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.pfx certificate file
  • Password for the .pfx file
๐Ÿ”ด

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

Your Backend: buildFormPayload() โ†’ RSA-SHA256 sign with CREDITOR.pfx private key โ†’ Returns ConnectIpsFormPayload (TOKEN field = signature) โœ… Save txnId to DB FIRST โ€” before returning to frontend Frontend: POST form with UPPERCASE field names to payload.formActionUrl() โ†“ User completes bank transfer on ConnectIPS gateway โ†“ ConnectIPS redirects to your callback URL ?txnId=xxx&referenceId=ORD-001&txnAmt=10000 โš ๏ธ NEVER trust this redirect alone โ€” it can be faked! Your Backend: validateTransaction(txnId, referenceId, txnAmtPaisa) โ†’ POST to ConnectIPS validate API with HTTP Basic Auth โ†’ { status: "SUCCESS", statusDesc: "TRANSACTION SUCCESSFUL" } โ†“ โœ… Mark order as paid ONLY when status == "SUCCESS"
โ„น๏ธ

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