How Khalti Works

Your Backend ──POST──▢ Khalti API
                         β”‚
                         β–Ό
                  returns { pidx, payment_url }
                         β”‚
                ← redirect user to payment_url
                         β”‚
                  user pays on Khalti
                         β”‚
           Khalti redirects to your return_url
                         β”‚
          ⚠️  NEVER trust this redirect alone!
                         β”‚
Your Backend ──POST──▢ Khalti Lookup (pidx)
                         β”‚
                  { status: "Completed" }
                         β”‚
                 βœ… mark order as paid

initiatePayment()

KhaltiInitiateResponse response = khaltiClient.initiatePayment(
    KhaltiInitiateRequest.builder()
        .amount(10000L)                 // NPR 100 = 10000 paisa
        .purchaseOrderId("ORD-001")     // your unique order ID
        .purchaseOrderName("Pro Plan")  // shown to user on Khalti
        .returnUrl("https://yourapp.com/callback")  // optional override
        .customerInfo(                  // optional
            "Sujan Lamichhane",
            "sujan@example.com",
            "9800000001"
        )
        .build()
);

// IMPORTANT: store pidx before redirecting user
orderRepo.savePidx(orderId, response.pidx());

// Redirect user
return redirect(response.paymentUrl());
⚠️ Amount is in Paisa, not NPR. NPR 100 = send 10000. NPR 10 minimum = send 1000.

lookupPayment()

// Call this AFTER receiving the Khalti redirect
// The redirect URL parameters can be faked β€” always verify server-side!

KhaltiLookupResponse lookup = khaltiClient.lookupPayment(pidx);

if (!lookup.isPaymentSuccessful()) {
    return "Payment failed: " + lookup.status();
}

// Optional but recommended: verify amount was not tampered
if (!lookup.isAmountValid(expectedPaisa)) {
    log.error("Amount mismatch β€” possible tampering!");
    return "Payment error";
}

// βœ… Safe to mark order as paid
orderRepo.markPaid(orderId, lookup.transactionId());

refundPayment()

⚠️ Refund uses transaction_id, not pidx. First call lookupPayment(pidx), then use lookup.transactionId() for the refund.

Khalti refunds are available for completed payments. NepalPay supports both full and partial refunds.

Full refund

// Step 1: Lookup payment using pidx
KhaltiLookupResponse lookup = khaltiClient.lookupPayment(pidx);

if (!lookup.isPaymentSuccessful()) {
    throw new IllegalStateException("Only completed payments can be refunded");
}

// Step 2: Get transactionId from lookup response
String transactionId = lookup.transactionId();

// Step 3: Refund full amount
KhaltiRefundResponse refund =
    khaltiClient.refundPayment(transactionId);

if (refund.isRefundSuccessful()) {
    orderRepo.markRefunded(orderId);
}

Partial refund

// Refund NPR 50 from a completed transaction
// Amount is in PAISA: NPR 50 = 5000 paisa

KhaltiRefundResponse refund =
    khaltiClient.refundPayment(transactionId, 5000L);

if (refund.isRefundSuccessful()) {
    orderRepo.markPartiallyRefunded(orderId);
}

Important refund rules

Rule Explanation
Use transactionId Refund API uses Khalti's internal transaction ID, not the pidx from payment initiation.
Get ID from lookup Call lookupPayment(pidx) and read lookup.transactionId().
Only completed payments Pending, expired, canceled, or failed payments cannot be refunded.
Amount is paisa Partial refund amount is in paisa. NPR 50 β†’ 5000L.
Full refund Use refundPayment(transactionId) with no amount.
πŸ”΄ Do not pass pidx into refundPayment(). It expects transactionId. Passing pidx will fail.
βœ… After refund succeeds, update your order/payment status in your own database to REFUNDED or PARTIALLY_REFUNDED.

Payment Statuses

Status isSuccess() isTerminalFailure() Action
COMPLETED βœ… true false Mark order as paid
REFUNDED false false Order was paid, then refunded. Mark as refunded.
PENDING false false Poll again later
USER_CANCELED false βœ… true Offer retry
CANCELED false βœ… true Offer retry
EXPIRED false βœ… true Generate new payment
FAILED false βœ… true Offer retry
UNKNOWN false false Contact Khalti support

Security Rules

πŸ”΄ Never trust redirect URL parameters alone. Always call lookupPayment(pidx) to confirm.
πŸ”΄ Never hardcode your secret key. Use environment variables.
βœ… Always store pidx in your database before redirecting the user.
βœ… Always call isAmountValid() to detect tampered amounts.