
August 12, 2026
11 min read
Table of Contents
By Kokil Thapa | Last reviewed: August 2026
Implementing Laravel Khalti and eSewa Nepal payment integration requires navigating two fundamentally different API architectures within a single application. While Khalti offers a modern RESTful interface suitable for direct server-to-server communication, eSewa’s legacy EPAY system relies on HMAC-signed form posts and asynchronous callbacks that demand rigorous validation. This guide provides the exact configuration, service classes, and security checks needed to build a reliable dual-gateway checkout for Nepali customers in 2026.
How do you architect Laravel Khalti and eSewa Nepal payment integration securely?
The most common mistake I see when developers approach Laravel payment integrations for Nepal is treating both gateways as interchangeable. They are not. A robust architecture isolates gateway logic behind a contract or interface, allowing your order processing logic to remain agnostic to the underlying provider. In my experience building eCommerce platforms like Nepal Gift Card and various legal-tech portals, this separation prevents vendor lock-in and simplifies testing.
Your directory structure should reflect this boundary. Create an App/Services/Payments namespace containing a PaymentGatewayInterface alongside concrete KhaltiService and EsewaService classes. Bind these in a service provider using contextual binding or a factory pattern based on user selection. Never place raw HTTP calls inside your controllers. When working on eCommerce projects in Nepal, I always enforce this structure because it makes swapping gateways or adding new ones (like IME Pay or ConnectIPS) a matter of adding a new class rather than refactoring core business logic.
Security starts at the architecture level. Store gateway credentials in .env only—never commit them. Use Laravel’s config caching in production to avoid repeated file reads. For eSewa specifically, your secret key must never be exposed to the frontend; all signing happens server-side before rendering the checkout form or redirecting.
How do you implement Khalti server-to-server verification in Laravel 12?
Khalti’s current API (v2) uses a straightforward REST pattern, but many tutorials still reference deprecated endpoints. As of 2026, you must use the /api/v2/epayment/initiate/ endpoint for initiation and /api/v2/epayment/lookup/ for verification. The critical detail often missed is that initiation returns a pidx (payment index), not a final confirmation. You cannot mark an order as paid until you successfully verify this pidx server-to-server.
Initiating the transaction
Create a dedicated service method that handles the initiation request. Use Laravel’s HTTP Client with explicit timeout and retry configuration. Network latency between Kathmandu servers and Khalti’s infrastructure can occasionally spike, so setting a 10-second timeout with one retry is practical.
<?php
namespace App\Services\Payments;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class KhaltiService implements PaymentGatewayInterface
{
public function initiate(array $orderData): array
{
$response = Http::withHeaders([
'Authorization' => 'Key ' . config('services.khalti.secret_key'),
'Content-Type' => 'application/json',
])
->timeout(10)
->retry(1, 100)
->post('https://a.khalti.com/api/v2/epayment/initiate/', [
'return_url' => route('payments.khalti.verify'),
'website_url' => config('app.url'),
'amount' => $orderData['amount'] * 100, // Khalti expects paisa
'purchase_order_id' => $orderData['order_id'],
'purchase_order_name' => $orderData['description'],
'customer_info' => [
'name' => $orderData['customer_name'],
'email' => $orderData['customer_email'],
'phone' => $orderData['customer_phone'],
],
]);
if ($response->failed()) {
Log::error('Khalti initiation failed', [
'status' => $response->status(),
'body' => $response->json(),
]);
throw new \RuntimeException('Payment initiation failed');
}
return $response->json();
}
} Note the amount conversion: Khalti operates in paisa, so multiply NPR amounts by 100. Store the returned pidx against your order record immediately. This identifier is your single source of truth for reconciliation.
Verifying payment server-side
After the user completes payment on Khalti’s hosted page, they return to your return_url with query parameters. Never trust these parameters alone. Always perform a server-to-server lookup using the stored pidx. This prevents attackers from crafting fake success URLs to unlock digital goods or services without paying—a vulnerability I’ve audited on multiple Nepali eCommerce sites.
public function verify(string $pidx): array
{
$response = Http::withHeaders([
'Authorization' => 'Key ' . config('services.khalti.secret_key'),
])
->timeout(10)
->get('https://a.khalti.com/api/v2/epayment/lookup/', [
'pidx' => $pidx,
]);
$data = $response->json();
if (!isset($data['status']) || $data['status'] !== 'Completed') {
throw new \RuntimeException('Payment not completed');
}
// Verify amount matches original order
$storedAmount = Order::where('pidx', $pidx)->value('amount_paisa');
if ($data['total_amount'] !== $storedAmount) {
Log::warning('Khalti amount mismatch', [
'pidx' => $pidx,
'expected' => $storedAmount,
'received' => $data['total_amount'],
]);
throw new \RuntimeException('Amount verification failed');
}
return $data;
} This double-check on amount is non-negotiable. If a user manipulates the initial request or if there’s a race condition, the amount verified against Khalti’s ledger must match what you charged. For Laravel developers in Nepal handling high-value transactions like legal service retainers or tour bookings, this verification step protects against both accidental discrepancies and deliberate fraud.
How do you handle eSewa EPAY HMAC signatures and callback validation?
eSewa’s EPAY integration is older and less developer-friendly than Khalti’s, but it remains essential because of its massive user base. The integration uses HMAC-SHA256 signatures for both outgoing requests and incoming callbacks. Getting this wrong means either rejecting legitimate payments or accepting fraudulent ones.
Generating the correct signature
eSewa’s signature string format is strictly defined and order-dependent. Concatenate the fields exactly as specified in their documentation: total_amount,transaction_uuid,product_code. Any deviation produces an invalid signature. Use PHP’s native hash_hmac with the raw output set to false (hex encoding).
class EsewaService implements PaymentGatewayInterface
{
public function generateSignature(float $amount, string $uuid, string $productCode): string
{
$signatureString = "{$amount},{$uuid},{$productCode}";
return hash_hmac(
'sha256',
$signatureString,
config('services.esewa.secret_key'),
false // hex output
);
}
public function getCheckoutUrl(): string
{
return app()->environment('production')
? 'https://epay.esewa.com.np/v2/form'
: 'https://uat.esewa.com.np/epay/main';
}
} A frequent pitfall: the UAT and production secret keys differ. Hardcoding the wrong key causes silent failures where signatures validate locally but reject in production. Always pull from environment-specific config.
Validating incoming callbacks
When eSewa redirects back to your success or failure URL, it appends transaction data as query parameters (for GET) or POST body fields. You must reconstruct the signature string from the received values and compare it against the provided signature using hash_equals to prevent timing attacks. Never use === for cryptographic comparison.
public function validateCallback(Request $request): bool
{
$receivedSignature = $request->input('signature');
$amount = $request->input('total_amount');
$uuid = $request->input('transaction_uuid');
$productCode = $request->input('product_code');
$expectedSignature = $this->generateSignature($amount, $uuid, $productCode);
if (!hash_equals($expectedSignature, $receivedSignature)) {
Log::warning('eSewa signature mismatch', [
'uuid' => $uuid,
'ip' => $request->ip(),
]);
return false;
}
// Additional check: verify status field
$status = $request->input('status');
if ($status !== 'COMPLETE') {
return false;
}
return true;
} I’ve encountered cases where merchants skipped signature validation entirely, relying only on the presence of a success URL parameter. Attackers exploited this to activate premium subscriptions without payment. For any REST API in Laravel that triggers post-payment fulfillment, signature validation is your primary defense.
What are the key differences between Khalti and eSewa for Laravel developers?
Choosing between gateways—or deciding to support both—depends on technical constraints as much as business needs. Having integrated both across multiple client projects, from florist shops to legal service portals, here is how they compare in practice for Laravel development in 2026.
| Criteria | Khalti | eSewa EPAY |
|---|---|---|
| API Style | RESTful JSON, Bearer auth | Form POST, HMAC-SHA256 |
| Verification Method | Server-to-server lookup via pidx | Callback signature validation |
| Sandbox Environment | Dedicated test keys + endpoints | Separate UAT domain + credentials |
| Refund Support | API-driven refund endpoint | Manual dashboard process only |
| Webhook Notifications | Supported (configure in dashboard) | Not available (callback-only) |
| User Base (Nepal) | Growing, strong urban demographic | Largest installed base nationwide |
| Integration Complexity | Lower (modern standards) | Higher (legacy patterns) |
| Settlement Time | T+1 typically | T+1 to T+2 |
In practice, I recommend supporting both for any consumer-facing Nepali eCommerce site. eSewa captures the broader market, especially outside Kathmandu, while Khalti offers superior developer experience and refund capabilities that matter for subscription services or digital products. For B2B or high-ticket legal-tech platforms like those I’ve built for law firms, Khalti’s cleaner audit trail and API refunds reduce operational overhead significantly.
How do you test Nepal payment gateways without live transactions?
Testing payment integrations safely requires disciplined use of sandbox environments and mock responses. Both Khalti and eSewa provide test credentials, but they behave differently and have distinct limitations.
- Khalti Sandbox: Use test keys from the Khalti merchant dashboard. The sandbox mirrors production API behavior closely, including error responses. Test cards like
9800000000trigger specific scenarios (success, insufficient balance, timeout). Always test the full initiate → verify cycle, not just initiation. - eSewa UAT: Access via
uat.esewa.com.npwith provided test merchant ID and secret. The UAT environment is less stable than production and occasionally resets test data. Keep a local log of successful test transaction UUIDs for debugging. Signature generation works identically to production, making it reliable for validating your HMAC logic. - Local Development: Never call live APIs during local development. Create fake implementations of your
PaymentGatewayInterfacethat return predictable responses. Use Laravel’s HTTP faking (Http::fake()) in feature tests to assert correct request formation without network calls. This speeds up your test suite and avoids accidental real charges. - Webhook Testing: For Khalti webhooks, use ngrok or Laravel Valet’s share feature to expose your local endpoint. Configure the tunnel URL in Khalti’s sandbox dashboard. For eSewa, since there are no webhooks, manually craft signed callback requests in Postman using your UAT secret to test your validation logic thoroughly.
A pattern I’ve found effective on SaaS products in Nepal is maintaining a dedicated PaymentTestService that simulates edge cases: expired tokens, amount mismatches, duplicate callbacks, and network timeouts. These scenarios rarely occur in happy-path sandbox testing but cause most production incidents. Invest time here upfront—it pays off during peak seasons like Dashain when transaction volume spikes and gateway stability decreases.
Laravel Khalti and eSewa Nepal Payment Integration Checklist
Shipping Laravel Khalti and eSewa Nepal payment integration to production demands more than working code. Use this checklist to verify your implementation covers security, reliability, and compliance before going live. These items come directly from issues I’ve resolved on real client deployments.
- Credentials isolated per environment: Production and sandbox keys stored separately in
.env.productionand.env.staging. No hardcoded values anywhere in codebase. - Amount verification enforced: Every successful callback or lookup compares gateway-reported amount against stored order amount before fulfillment.
- Idempotent processing: Duplicate callbacks (common with eSewa) handled gracefully using database unique constraints on transaction IDs. Orders updated only once.
- Logging comprehensive but safe: All gateway interactions logged with request/response metadata. Sensitive data (full phone numbers, tokens) masked or excluded.
- Timeouts and retries configured: HTTP clients have explicit timeouts (10s recommended). Retries limited to idempotent operations only—never retry payment initiation blindly.
- Error messages user-friendly: Frontend displays actionable messages (“Payment incomplete, please try again”) instead of raw API errors or stack traces.
- Database transactions used: Order status updates and fulfillment actions wrapped in DB transactions to prevent partial states if verification succeeds but save fails.
- Monitoring active: Failed verification attempts, signature mismatches, and timeout rates tracked via Laravel logs or external monitoring. Alerts configured for anomaly thresholds.
If you’re building a Laravel eCommerce system with POS integration, add reconciliation jobs that run nightly to compare your local transaction records against gateway settlement reports. Discrepancies caught early prevent accounting headaches and customer disputes. For legal-tech platforms handling sensitive payments like court marriage fees or notary retainers, consider adding an admin approval step between payment verification and service activation—this extra human check catches edge cases automated systems miss.
Reliable Laravel Khalti and eSewa Nepal payment integration is achievable with disciplined engineering, but the margin for error is narrow. If you need help auditing your existing implementation, setting up secure webhook handling, or integrating additional Nepali payment methods into your Laravel application, get in touch to discuss your project requirements.

