Developers in Nepal integrating digital payment processing with Khalti's modern REST ePayment API.
- ✓Node.js or Laravel environment
- ✓Understanding of Bearer / Key authorization headers
- ✓Khalti merchant account or test sandbox keys
🚀 What You Will Learn & Build
- ▸How Khalti's modern ePayment v2 workflow functions
- ▸Initiating payments using the /epayment/initiate/ endpoint
- ▸Understanding the payment identifier (pidx)
- ▸Redirecting users to payment_url securely
- ▸Verifying payment status using /epayment/lookup/
- ▸Handling edge cases like canceled payments and duplicate callbacks
1. Understanding Khalti ePayment v2 Architecture
2. Khalti Sandbox Credentials & Configuration
# Khalti Credentials
KHALTI_SECRET_KEY=live_secret_key_68791341fdd94846a146f0457f9b6e1b # Replace with your test/live key
KHALTI_API_BASE=https://a.khalti.com/api/v2
APP_URL=http://localhost:30003. Initiating the Payment (Server-Side)
const axios = require('axios');
async function initiateKhaltiPayment({ returnUrl, websiteUrl, amountNpr, purchaseOrderId, purchaseOrderName, customerInfo }) {
const url = `${process.env.KHALTI_API_BASE}/epayment/initiate/`;
// Convert NPR to Paisa (Rs 10 = 1000 Paisa)
const amountInPaisa = Math.round(Number(amountNpr) * 100);
const payload = {
return_url: returnUrl,
website_url: websiteUrl,
amount: amountInPaisa,
purchase_order_id: purchaseOrderId,
purchase_order_name: purchaseOrderName,
customer_info: customerInfo || {
name: "Test Customer",
email: "test@example.com",
phone: "9800000000"
}
};
const response = await axios.post(url, payload, {
headers: {
Authorization: `Key ${process.env.KHALTI_SECRET_KEY}`,
'Content-Type': 'application/json'
}
});
// response.data contains: { pidx: "...", payment_url: "https://...", expires_at: "..." }
return response.data;
}
module.exports = { initiateKhaltiPayment };4. Verifying Payment with the Lookup API
router.get('/khalti/callback', async (req, res) => {
const { pidx, status, transaction_id } = req.query;
if (!pidx) return res.status(400).send('Missing pidx in callback');
try {
const lookupRes = await axios.post(
`${process.env.KHALTI_API_BASE}/epayment/lookup/`,
{ pidx },
{
headers: {
Authorization: `Key ${process.env.KHALTI_SECRET_KEY}`,
'Content-Type': 'application/json'
}
}
);
const data = lookupRes.data;
if (data.status === 'Completed') {
// 1. Verify data.total_amount matches expected order amount
// 2. Mark order as Paid in database
// 3. Prevent duplicate fulfillment
return res.redirect(`/order-success?pidx=${pidx}`);
} else {
return res.redirect(`/order-failed?status=${data.status}`);
}
} catch (error) {
console.error('Khalti lookup error:', error.response?.data || error.message);
return res.status(500).send('Payment verification failed');
}
});✨ Expected Output & Verification
Successful initiation returns: { "pidx": "bF8t25Xq937gM", "payment_url": "https://test-pay.khalti.com/?pidx=bF8t25Xq937gM", "expires_at": "2025-01-12T10:15:30Z", "expires_in": 1800 } Successful lookup returns: { "pidx": "bF8t25Xq937gM", "total_amount": 1000, "status": "Completed", "transaction_id": "T1234567890", "fee": 0, "refunded": false }
🧪 How to Test the Implementation
- 1Use Khalti test wallet credentials provided in Khalti docs (Mobile: 9800000000 to 9800000005, MPIN: 1111, OTP: 987654)
- 2Initiate payment and verify browser redirects to https://test-pay.khalti.com
- 3Enter credentials and confirm checkout
- 4Verify that return_url receives pidx and server completes lookup successfully
⚠️ Common Errors & How to Fix Them
Root Cause: Missing or incorrect 'Key <secret_key>' header in authorization request.
Fix: Format the header as 'Authorization: Key your_secret_key_here' with a space after 'Key'.
Root Cause: Submitting rupees instead of paisa (e.g. submitting 10 instead of 1000).
Fix: Multiply rupee amounts by 100 and cast to an integer before sending payload.
Root Cause: User stayed on checkout screen longer than 30 minutes.
Fix: Handle Expired status by generating a fresh payment session.
🛡️ Security Considerations & Best Practices
- !Never put the Khalti secret key inside client-side JS or HTML.
- !Verify the transaction amount returned by /lookup/ matches your database order value in Paisa.
- !Implement idempotent database operations to prevent crediting orders twice.
❓ Frequently Asked Questions (FAQ)
Yes, Khalti's hosted payment page allows users to pay via Khalti Wallet, ConnectIPS, Mobile Banking (all major Nepali banks), and SCT / Visa cards.
📌 Summary of Key Takeaways
- ✓Khalti v2 uses simple JSON REST endpoints: /initiate/ and /lookup/.
- ✓Always supply amounts in Paisa (NPR * 100).
- ✓Perform server-to-server verification with the pidx before marking orders as paid.
Official References & Further Reading
- 🔗Official Khalti Documentation— Official developer docs and API references.
- 🔗Khalti ePayment API v2 Reference— Initiate and lookup API parameters and response specifications.
Written by Manoj Neupane
AuthorSoftware developer and programming instructor, Nepal
Building software systems and SaaS platforms with Laravel, Next.js, and Node.js. Passionate about helping students and developers in Nepal master practical, real-world development skills.