Requirements

โš ๏ธ Merchant registration required. ConnectIPS requires registration with NCHL before use. You cannot test or go live without NCHL credentials.

You will need from NCHL:

  • Merchant ID (integer)
  • Application ID and Application Name
  • Application Password
  • CREDITOR.pfx certificate file
  • Password for the .pfx file

To register, contact your bank or NCHL directly: connectips@nchl.com.np

How ConnectIPS Works

โ„น๏ธ ConnectIPS uses a form-submission model similar to eSewa, but uses RSA-SHA256 digital signatures instead of HMAC-SHA256.
Backend: builds ConnectIpsFormPayload
         (RSA-SHA256 signed with your .pfx private key)
    โ†“ return to frontend
Frontend: POSTs form to ConnectIPS gateway URL
    โ†“
User completes bank transfer on ConnectIPS
    โ†“
ConnectIPS redirects to your callback URL
    โ†“
โš ๏ธ  DO NOT trust this redirect alone!
    โ†“
Backend: calls validateTransaction() with Basic Auth
         (uses appId:appPassword for HTTP Basic Auth)
    โ†“
โœ… mark order as paid when status = "SUCCESS"

Key Difference from Khalti and eSewa

Feature Khalti eSewa ConnectIPS
Flow type API-first Form submit Form submit
Signature None (API key) HMAC-SHA256 RSA-SHA256 (.pfx)
Amount unit Paisa NPR Paisa
Verify API auth Bearer key No auth needed HTTP Basic Auth
Registration Self-service Self-service Via NCHL/bank

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}        # e.g. "MyStore"
    app-password: ${CONNECTIPS_APP_PASSWORD}
    pfx-path:     ${CONNECTIPS_PFX_PATH}       # e.g. file:/app/CREDITOR.pfx
    pfx-password: ${CONNECTIPS_PFX_PASSWORD}
    sandbox: true    # false for production
โš ๏ธ Never commit your .pfx file to Git. Add it to .gitignore and load it via environment variable or a secrets manager.

buildFormPayload()

@Service
@RequiredArgsConstructor
public class PaymentService {

    private final ConnectIpsClient connectIpsClient;

    public ConnectIpsFormPayload startPayment(String orderId, long amountNPR) {
        String txnId = "TXN-" + orderId + "-" + System.currentTimeMillis();

        // ConnectIPS uses PAISA โ€” multiply by 100
        long amountPaisa = amountNPR * 100L;

        // Store txnId in your database before returning!
        ConnectIpsFormPayload payload = connectIpsClient.buildFormPayload(
            txnId,
            amountPaisa,
            orderId,           // referenceId โ€” your order ID
            "Order payment",   // remarks
            "NepalPay"         // particulars
        );

        return payload;
        // Return to frontend โ†’ frontend POSTs form to payload.formActionUrl()
    }
}
โš ๏ธ Amount is in Paisa. ConnectIPS uses the same unit as Khalti. NPR 100 โ†’ send 10000.

Frontend Form Submission

ConnectIPS uses the same form-submission pattern as eSewa. POST all payload fields to form_action_url.

// Angular/TypeScript:
initiateConnectIps(payload: ConnectIpsFormPayload): void {
    const form = document.createElement('form');
    form.method = 'POST';
    form.action = payload.form_action_url;

    const fields = ['MERCHANTID', 'APPID', 'APPNAME', 'TXNID',
        'TXNDATE', 'TXNCRNCY', 'TXNAMT', 'REFERENCEID',
        'REMARKS', 'PARTICULARS', 'TOKEN'];

    fields.forEach(field => {
        const input = document.createElement('input');
        input.type = 'hidden';
        input.name = field;
        input.value = (payload as any)[field.toLowerCase()] ?? '';
        form.appendChild(input);
    });

    document.body.appendChild(form);
    form.submit();
}

validateTransaction()

// ConnectIPS redirects to your callback URL after payment
// Always verify server-side โ€” never trust redirect alone!

@GetMapping("/connectips/callback")
public ResponseEntity<String> callback(
        @RequestParam String txnId,
        @RequestParam String referenceId,
        @RequestParam long txnAmt) {

    ConnectIpsValidateResponse response =
        connectIpsClient.validateTransaction(txnId, referenceId, txnAmt);

    if (!response.isPaymentSuccessful()) {
        return ResponseEntity.badRequest()
            .body("Payment not confirmed: " + response.statusDesc());
    }

    // โœ… Safe to mark order as paid
    orderRepo.markPaid(referenceId);
    return ResponseEntity.ok("Payment successful");
}

Security Notes

๐Ÿ”ด Never commit your CREDITOR.pfx file to Git. It contains your private key. Add it to .gitignore.
๐Ÿ”ด Never trust the ConnectIPS redirect alone. Always call validateTransaction() server-side.
โœ… Store txnId in your database before returning the form payload to the frontend.
โœ… The RSA signature generation uses your .pfx private key and happens entirely on your server โ€” it never reaches the frontend.