Integrating eSewa, Khalti, and Fonepay into Custom PHP Web Applications
Digital payments in Nepal have matured rapidly. Today, eSewa, Khalti, and Fonepay process millions of rupees in transactions daily across retail, service booking, and utility payments. For custom web applications built with pure PHP, Laravel, or custom MVC frameworks, understanding the low-level API mechanics, HMAC-SHA256 signatures, and server-side verification callbacks is essential to prevent financial discrepancies and fraudulent orders.
Strategic Executive Summary
- Core Insight: Never fulfill an order or grant account credits solely based on URL query parameters returned in a user's browser. Always execute an independent server-to-server verification request to validate transaction status, exact amount, and transaction UUID.
- Production Quality: Battle-tested engineering techniques designed specifically for Nepal's network infrastructure and business environment.
- Direct Implementation: Copy-paste ready code architectures with security safeguards against race conditions, data corruption, and unauthorized access.
Table of Contents
1. Integrating eSewa ePay v2 with HMAC-SHA256 Signatures
The eSewa ePay v2 protocol requires developers to sign payment requests using HMAC-SHA256. The signed message combines total amount, transaction UUID, and merchant product code. Here is the complete production PHP implementation:
<?php
// eSewa v2 Payment Initiation
$secret_key = '8gBm/:&EnhH.1/q('; // Use your production merchant secret key
$product_code = 'EPAYTEST'; // Your eSewa merchant code
$transaction_uuid = 'TXN-' . time() . '-' . rand(1000, 9999);
$total_amount = '1500.00'; // NPR 1,500.00
// Generate HMAC-SHA256 signature
$signature_data = "total_amount={$total_amount},transaction_uuid={$transaction_uuid},product_code={$product_code}";
$raw_hash = hash_hmac('sha256', $signature_data, $secret_key, true);
$signature = base64_encode($raw_hash);
// Build automatic redirection form
echo '<form id="esewaForm" action="https://rc-epay.esewa.com.np/api/epay/main/v2/form" method="POST">';
echo '<input type="hidden" name="amount" value="' . $total_amount . '">';
echo '<input type="hidden" name="tax_amount" value="0">';
echo '<input type="hidden" name="total_amount" value="' . $total_amount . '">';
echo '<input type="hidden" name="transaction_uuid" value="' . $transaction_uuid . '">';
echo '<input type="hidden" name="product_code" value="' . $product_code . '">';
echo '<input type="hidden" name="product_service_charge" value="0">';
echo '<input type="hidden" name="product_delivery_charge" value="0">';
echo '<input type="hidden" name="success_url" value="https://safalbhurtel.site/payment/esewa-success.php">';
echo '<input type="hidden" name="failure_url" value="https://safalbhurtel.site/payment/esewa-failure.php">';
echo '<input type="hidden" name="signed_field_names" value="total_amount,transaction_uuid,product_code">';
echo '<input type="hidden" name="signature" value="' . $signature . '">';
echo '<button type="submit">Redirecting to eSewa...</button>';
echo '</form>';
echo '<script>document.getElementById("esewaForm").submit();</script>';
On return to esewa-success.php, decode the base64 payload returned in $_GET['data'] and verify the status directly against eSewa's transaction status endpoint:
<?php
// Verify status with eSewa server API
$verify_url = "https://rc.esewa.com.np/api/epay/transaction/status/?product_code={$product_code}&total_amount={$total_amount}&transaction_uuid={$transaction_uuid}";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $verify_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
if (isset($data['status']) && $data['status'] === 'COMPLETE') {
// Transaction is verified. Mark order as PAID in MySQL.
}
2. Khalti ePayment v2 REST API
Khalti utilizes a clean, modern JSON REST API. Payments are initiated server-side, returning a unique payment identifier (pidx) and a payment URL:
<?php
$khalti_secret_key = 'live_secret_key_xxxxxxxxxxxxxxxx';
$payload = array(
"return_url" => "https://safalbhurtel.site/payment/khalti-callback.php",
"website_url" => "https://safalbhurtel.site/",
"amount" => 150000, // Amount in Paisa (NPR 1,500 = 150,000 paisa)
"purchase_order_id" => "ORDER-10024",
"purchase_order_name" => "Custom Web Development Deposit",
"customer_info" => array(
"name" => "Safal Bhurtel",
"email" => "bhurtelsafal07@gmail.com",
"phone" => "9766778033"
)
);
$ch = curl_init('https://khalti.com/api/v2/epayment/initiate/');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Authorization: Key ' . $khalti_secret_key,
'Content-Type: application/json'
));
$response = curl_exec($ch);
curl_close($ch);
$res = json_decode($response, true);
if (isset($res['pidx']) && isset($res['payment_url'])) {
// Store $res['pidx'] in MySQL orders table
header('Location: ' . $res['payment_url']);
exit;
}
When Khalti redirects back to khalti-callback.php with $_GET['pidx'], execute the lookup endpoint:
<?php
// Lookup payment status using pidx
$pidx = $_GET['pidx'];
$ch = curl_init('https://khalti.com/api/v2/epayment/lookup/');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(array('pidx' => $pidx)));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Authorization: Key ' . $khalti_secret_key,
'Content-Type: application/json'
));
$lookup_res = json_decode(curl_exec($ch), true);
curl_close($ch);
if ($lookup_res['status'] === 'Completed' && $lookup_res['total_amount'] == 150000) {
// Transaction verified. Update order status atomically.
}
3. Fonepay Merchant-Hosted & Dynamic QR
Fonepay is essential for direct bank-to-bank interoperability in Nepal. For desktop e-commerce and point-of-sale systems, generating dynamic QR codes linked to Fonepay allows users to scan from any mobile banking app (Global Smart Plus, NIC Asia MoBank, Nabil SmartBank, etc.).
The integration involves sending merchant code, bill number, and amount to Fonepay's API to receive an encoded EMVCo QR string, which your frontend renders using JavaScript QR libraries. Fonepay then transmits an asynchronous server-to-server webhook to your configured listener.
4. Preventing Race Conditions and Double-Crediting
In high-volume stores, users may refresh redirect pages, or webhook retries may trigger simultaneously. Protect database updates using atomic SQL transactions and unique database constraints:
$pdo->beginTransaction();
$stmt = $pdo->prepare("SELECT status FROM orders WHERE id = :id FOR UPDATE");
$stmt->execute(['id' => $order_id]);
$current_status = $stmt->fetchColumn();
if ($current_status === 'pending') {
$update = $pdo->prepare("UPDATE orders SET status = 'completed', gateway_txn_id = :txn WHERE id = :id");
$update->execute(['txn' => $transaction_uuid, 'id' => $order_id]);
$pdo->commit();
} else {
$pdo->rollBack(); // Already processed
}