PayBridgeNP
KhaltiDeveloperPayment gatewayNepal

Khalti API Integration in Nepal: A Developer's Guide (2026)

How Khalti's ePayment API works for developers: initiate, the payment page, the return URL, the lookup that confirms payment and its statuses, sandbox test accounts, the website_url trap, and the shorter path through one API.

October 22, 20266 min readPayBridgeNP Team
Khalti API Integration in Nepal: A Developer's Guide (2026)

Khalti's web checkout is the friendliest of Nepal's wallet APIs to build against: plain JSON over HTTPS, amounts in paisa, and one lookup call that tells you whether the money arrived. It still has a handful of details that turn into support tickets if you miss them, from a field Khalti will not accept blank to a test wallet that is empty because everyone else used it first.

This guide covers Khalti's ePayment flow end to end, the way we run it in production.

Last checked: 22 October 2026, against the Khalti integration PayBridgeNP runs for live merchants. Need the merchant account itself first? That is in how to get a Khalti merchant account.

How a Khalti payment works

  1. Your server starts the payment with a call to Khalti's initiate endpoint. Khalti answers with a pidx (its ID for this payment) and a payment_url.
  2. The customer pays on Khalti's page at that URL, with their Khalti wallet, and is sent back to your return_url.
  3. Your server confirms the payment with Khalti's lookup endpoint, using the pidx, before you mark the order paid.
Test (sandbox)Live
API basehttps://dev.khalti.com/api/v2https://khalti.com/api/v2
Start a paymentPOST /epayment/initiate/POST /epayment/initiate/
Confirm a paymentPOST /epayment/lookup/POST /epayment/lookup/
Authorisation headerKey <your test secret key>Key <your live secret key>

Note the header format: the word Key, a space, then your secret key. Not Bearer.

Step 1: start the payment

Send JSON to /epayment/initiate/ from your server:

{
  "return_url": "https://yourstore.com/order/confirm",
  "website_url": "https://yourstore.com",
  "amount": 10000,
  "purchase_order_id": "order-1042",
  "purchase_order_name": "Order 1042"
}
FieldWhat goes in it
amountIn paisa: 10000 is Rs 100
purchase_order_idYour order or attempt ID
purchase_order_nameA short label the customer sees. Keep it short and never empty
return_urlWhere Khalti sends the customer after paying or cancelling
website_urlYour site's address. Required, and must not be blank
customer_infoOptional name, email and phone, shown to the customer on Khalti's page

The response carries pidx and payment_url. Store the pidx with the order, then send the customer to payment_url.

Step 2: the return URL

When the customer finishes, Khalti redirects them to your return_url with query parameters including pidx, status, transaction_id, amount and purchase_order_id.

A status of User canceled means the customer backed out. For anything else, do not trust the URL: anyone can open it with any values they like. Check that its pidx is the one you stored for this order, then ask Khalti about that stored pidx. A pidx you did not issue for this order must never confirm it.

Step 3: confirm with lookup

Post the pidx to /epayment/lookup/ with the same Key header:

{ "pidx": "the-pidx-from-initiate" }

Only Completed with refunded false means paid. Check that total_amount in the answer is the amount you asked for, and fulfil each order once, even if the same pidx is confirmed again. The statuses you will meet:

StatusWhat it means for the order
CompletedPaid, unless refunded is true. Check the amount, then fulfil once
Pending, InitiatedNot finished. Check again later; do not fail the order
User canceledThe customer cancelled
ExpiredThe payment link ran out before the customer paid
Refunded, Partially RefundedRefunded in full or in part (Khalti's docs spell the partial one with both capital and small r)

If the lookup itself times out or Khalti answers with a server error, the customer may still have paid. Treat it as "not known yet" and ask again, never as a failed payment.

The traps that cause most Khalti bugs

website_url cannot be blank. Khalti rejects the initiate call with a 400 if it is empty. This bites flows that deliberately have no return page, such as payment links and invoices: send your site's address anyway.

Amounts are paisa, unlike eSewa. Khalti takes 10000 for Rs 100. If you integrate eSewa as well, which takes rupees as 100.00, convert at each provider's edge and test both with an amount that has paisa in it.

Do not trust the return URL. It is a convenience for the customer, not proof of payment. Confirm with lookup, or from a signed webhook if you use a service that sends one.

The shared test wallets run dry. Every developer in Nepal tests against the same Khalti test accounts, so some of them are often empty or busy. If you see "insufficient balance" or "similar request already being processed" in the sandbox, that is the sandbox, not your code: switch test accounts.

Refunds go through Khalti. Khalti supports refunds through its merchant dashboard and API. Decide early who in your team issues them and how the customer hears about it.

Testing in the sandbox

Get test keys from Khalti's test merchant setup (the Khalti docs walk through it), point your code at https://dev.khalti.com/api/v2, and pay on the test page with a shared test account:

FieldValue
Khalti ID9800000005 (the most reliable of the shared accounts)
MPIN1111
OTP987654

Run the success path, a cancellation, and a lookup on a pidx you never paid, so you have seen Pending and Expired before a customer shows them to you.

The shorter path: one API for Khalti, eSewa and Fonepay

Khalti on its own is a manageable integration. Most Nepali shops also want eSewa and Fonepay, which sign, redirect and confirm in completely different ways.

With PayBridgeNP you connect your own Khalti merchant account once and create one checkout session for any provider:

import { PayBridgeNP } from "@paybridge-np/sdk";

const paybridge = new PayBridgeNP({ apiKey: process.env.PAYBRIDGENP_API_KEY! });

// Send the customer straight to Khalti
const session = await paybridge.checkout.create({
  amount: 10000, // NPR 100.00, in paisa
  currency: "NPR",
  provider: "khalti",
  flow: "redirect",
  returnUrl: "https://yourstore.com/order/confirm",
  customer: { name: "Hari Gurung", email: "hari@example.com" },
});

// Send the customer to session.checkout_url
// Confirm the payment from the webhook, never from the return URL
console.log(session.checkout_url);

The initiate call, the lookup against the stored pidx and the amount check are handled for you, and your server gets one signed payment.succeeded webhook whichever provider the customer used. Leave out both provider and flow and the customer picks Khalti, eSewa or Fonepay on a hosted page; each provider needs your own merchant account with it. Payments settle straight into your own Khalti merchant account; PayBridgeNP never holds the money, and refunds are a button in the dashboard.

Next Steps


Ready to accept payments in Nepal?

Connect eSewa, Khalti, and Fonepay with a single API. Free to start, with no fund-holding and no new bank accounts.