
Integration of eSewa and Khalti Payment Gateways in Next.js 15
Thu Jan 22 2026
Digital payments have become an essential part of modern applications in Nepal. Among the most popular options are eSewa and Khalti, which allow businesses to provide secure, quick, and reliable online transactions.
In this article, we’ll walk through how to integrate both eSewa and Khalti in a Next.js 15 project. We’ll build a minimal but production-friendly integration with:
-
Secure API routes
-
Client-side forms for initiating payments
-
Success and failure pages
-
Test credentials for sandbox mode
By the end, you’ll have a working implementation that you can extend for your own projects.
You can check out the live demo and source code here and dont forget to star the repo:
-
Live Demo: https://esewa-khalti-nextjs-integration.vercel.app/
-
GitHub Repo: https://github.com/amrit-sapkota/eSewa-and-khalti-nextjs-integration
⚙️ Project Setup
Start by creating a new Next.js 15 project:
npx create-next-app@latest esewa-khalti-integration
cd esewa-khalti-integration
Install dependencies we’ll need:
npm install uuid crypto-js
We’ll also use Tailwind CSS for styling (optional, but helps make the UI cleaner).
🔑 Environment Variables
Create a .env.local file in the root of your project and add the following variables:
NEXT_PUBLIC_BASE_URL=http://localhost:3000
# eSewa (Test Mode)
NEXT_PUBLIC_ESEWA_MERCHANT_CODE=YOUR_ESEWA_MERCHANT_CODE
NEXT_PUBLIC_ESEWA_SECRET_KEY=YOUR_ESEWA_SECRET_KEY
# Khalti (Test Mode)
NEXT_PUBLIC_KHALTI_SECRET_KEY=YOUR_KHALTI_SECRET_KEY
These values come from your developer account dashboards. For testing, both eSewa and Khalti provide sandbox credentials.
API Route: Initiate Payment
We’ll use a single API route that handles both eSewa and Khalti payment initiations. This way, we centralize logic and keep our frontend clean.
// /app/api/initiate-payment/route.ts
import { generateEsewaSignature } from "@/lib/generate-signtaure";
import { NextResponse } from "next/server";
import { v4 as uuidv4 } from "uuid";
export async function POST(req: Request) {
try {
const { amount, productName, transactionId, method } = await req.json();
if (!amount || !productName || !transactionId || !method) {
return NextResponse.json({ error: "Missing fields" }, { status: 400 });
}
switch (method) {
case "esewa": {
const transactionUuid = `${Date.now()}-${uuidv4()}`;
const esewaConfig = {
amount,
tax_amount: "0",
total_amount: amount,
transaction_uuid: transactionUuid,
product_code: process.env.NEXT_PUBLIC_ESEWA_MERCHANT_CODE!,
product_service_charge: "0",
product_delivery_charge: "0",
success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/success?method=esewa`,
failure_url: `${process.env.NEXT_PUBLIC_BASE_URL}/failure`,
signed_field_names: "total_amount,transaction_uuid,product_code",
};
const signatureString = `total_amount=${esewaConfig.total_amount},transaction_uuid=${esewaConfig.transaction_uuid},product_code=${esewaConfig.product_code}`;
const signature = generateEsewaSignature(
process.env.NEXT_PUBLIC_ESEWA_SECRET_KEY!,
signatureString
);
return NextResponse.json({
esewaConfig: { ...esewaConfig, signature },
});
}
case "khalti": {
const khaltiConfig = {
return_url: `${process.env.NEXT_PUBLIC_BASE_URL}/success?method=khalti`,
website_url: process.env.NEXT_PUBLIC_BASE_URL!,
amount: Math.round(parseFloat(amount) * 100), // convert to paisa
purchase_order_id: transactionId,
purchase_order_name: productName,
customer_info: {
name: "Test User",
email: "test@test.com",
phone: "9800000000",
},
};
const response = await fetch(
"https://a.khalti.com/api/v2/epayment/initiate/",
{
method: "POST",
headers: {
Authorization: `Key ${process.env.NEXT_PUBLIC_KHALTI_SECRET_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(khaltiConfig),
}
);
if (!response.ok) {
return NextResponse.json(
{ error: "Khalti initiation failed" },
{ status: 500 }
);
}
const data = await response.json();
return NextResponse.json({ khaltiPaymentUrl: data.payment_url });
}
default:
return NextResponse.json({ error: "Invalid method" }, { status: 400 });
}
} catch (err) {
return NextResponse.json(
{ error: "Server error", details: String(err) },
{ status: 500 }
);
}
}
Why?
- eSewa requires creating a signature to validate requests.
import CryptoJS from "crypto-js";
export function generateEsewaSignature(
secretKey: string,
message: string
): string {
const hash = CryptoJS.HmacSHA256(message, secretKey);
return CryptoJS.enc.Base64.stringify(hash);
}
- Khalti provides a
payment_urlafter initiation that we redirect users to.
Frontend: Payment Forms
We’ll create two forms: one for eSewa and one for Khalti. A toggle lets users switch between them.
You can make Toggle page where user can select eSewa and Khalti and link it to respective forms. Here is the sample UI for the respective forms. Feel free to customize the UI — you can make it as simple or fancy as you want.
eSewa Form
"use client";
import { useState } from "react";
export default function EsewaPayment() {
const [amount, setAmount] = useState("100");
const [productName, setProductName] = useState("Test Product");
const [transactionId, setTransactionId] = useState("txn-123");
const handlePayment = async (e: React.FormEvent) => {
e.preventDefault();
const res = await fetch("/api/initiate-payment", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ method: "esewa", amount, productName, transactionId }),
});
const data = await res.json();
if (data.esewaConfig) {
const form = document.createElement("form");
form.method = "POST";
form.action = "https://rc-epay.esewa.com.np/api/epay/main/v2/form";
Object.entries(data.esewaConfig).forEach(([k, v]) => {
const input = document.createElement("input");
input.type = "hidden"; input.name = k; input.value = String(v);
form.appendChild(input);
});
document.body.appendChild(form);
form.submit();
}
};
return (
<form onSubmit={handlePayment} className="p-6 bg-white rounded-xl shadow">
<h2 className="text-lg font-semibold">Pay with eSewa</h2>
{/* Fields */}
<input value={amount} onChange={(e) => setAmount(e.target.value)} className="border p-2 w-full mt-2" />
<input value={productName} onChange={(e) => setProductName(e.target.value)} className="border p-2 w-full mt-2" />
<input value={transactionId} onChange={(e) => setTransactionId(e.target.value)} className="border p-2 w-full mt-2" />
<button type="submit" className="bg-green-500 text-white mt-3 py-2 px-4 rounded">Pay</button>
</form>
);
}
Khalti Form
"use client";
import { useState } from "react";
export default function KhaltiPayment() {
const [amount, setAmount] = useState("100");
const [productName, setProductName] = useState("Test Product");
const [transactionId, setTransactionId] = useState("txn-456");
const handlePayment = async (e: React.FormEvent) => {
e.preventDefault();
const res = await fetch("/api/initiate-payment", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ method: "khalti", amount, productName, transactionId }),
});
const data = await res.json();
if (data.khaltiPaymentUrl) window.location.href = data.khaltiPaymentUrl;
};
return (
<form onSubmit={handlePayment} className="p-6 bg-white rounded-xl shadow">
<h2 className="text-lg font-semibold">Pay with Khalti</h2>
<input value={amount} onChange={(e) => setAmount(e.target.value)} className="border p-2 w-full mt-2" />
<input value={productName} onChange={(e) => setProductName(e.target.value)} className="border p-2 w-full mt-2" />
<input value={transactionId} onChange={(e) => setTransactionId(e.target.value)} className="border p-2 w-full mt-2" />
<button type="submit" className="bg-purple-600 text-white mt-3 py-2 px-4 rounded">Pay</button>
</form>
);
}
Redirecting Users After Payment
After the payment process is completed, users should be redirected to the appropriate page depending on the outcome:
-
Success Case: Redirect the user to a success URL (e.g.,
/success). -
Failure Case: Redirect the user to a failure URL (e.g.,
/failure).
In Next.js, this can be achieved by creating separate routes (pages) for each condition. For example:
-
app/success/page.tsx→ Displays a success message when the payment is successful. -
app/failure/page.tsx→ Displays an error message when the payment fails.
This ensures that users get clear feedback after completing the payment flow.
Test Credentials
eSewa:
-
ID:
9806800001 / 2 / 3 / 4 / 5 -
Password:
Nepal@123 -
MPIN:
1122 -
Token:
123456
Khalti:
-
Test IDs:
9800000000 - 9800000005 -
MPIN:
1111 -
OTP:
987654
Official docs:
Wrapping Up
We just built a complete eSewa + Khalti integration in Next.js 15.
-
Payments are initiated securely via API routes.
-
Users can toggle between gateways.
-
We handle redirects with success/failure pages.