10 August 2026 Payment Gateways, PHP, API, Nepal Fintech

Integrating eSewa, Khalti, and Fonepay into Custom PHP Web Applications

eSewa Khalti and Fonepay Payment Gateway API Integration in PHP - Developer Safal Bhurtel Butwal

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. 1. Integrating eSewa ePay v2 with HMAC-SHA256 Signatures
  2. 2. Khalti ePayment v2 REST API
  3. 3. Fonepay Merchant-Hosted & Dynamic QR
  4. 4. Preventing Race Conditions and Double-Crediting

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:

eSewa v2 Payment Initiation PHP 8.3
<?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:

Verify status with eSewa server API PHP 8.3
<?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:

safalbhurtel.site PHP 8.3
<?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:

Lookup payment status using pidx PHP 8.3
<?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:

schema.sql MySQL 8.0
$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
}
Safal Bhurtel

Safal Bhurtel

Full-Stack Web Developer • Butwal, Nepal

Safal Bhurtel has 6+ years of specialized web engineering experience developing custom PHP/MySQL web applications, high-converting WordPress/WooCommerce websites, API payment integrations (eSewa, Khalti, Fonepay), and speed-optimized digital solutions across Nepal.