Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Laravel Khalti and eSewa Nepal Payment Integration

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.

Laravel AppOrder ControllerPayment Interfaceinitiate() / verify()Gateway ContractKhalti ServiceREST + Bearer TokeneSewa ServiceHMAC + Form Post
Unified payment interface decouples Laravel order logic from specific Khalti or eSewa implementation details

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.

Laravel AppeSewa GatewayCustomerPOST + HMAC SigRedirect to PayCompletesCallback + DataVerify HMAChash_hmac('sha256',$data, $secret)Update Order
eSewa EPAY flow requires HMAC verification on every callback to prevent transaction tampering

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.

CriteriaKhaltieSewa EPAY
API StyleRESTful JSON, Bearer authForm POST, HMAC-SHA256
Verification MethodServer-to-server lookup via pidxCallback signature validation
Sandbox EnvironmentDedicated test keys + endpointsSeparate UAT domain + credentials
Refund SupportAPI-driven refund endpointManual dashboard process only
Webhook NotificationsSupported (configure in dashboard)Not available (callback-only)
User Base (Nepal)Growing, strong urban demographicLargest installed base nationwide
Integration ComplexityLower (modern standards)Higher (legacy patterns)
Settlement TimeT+1 typicallyT+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.

Start: Choose GatewayNeed API refunds or webhooks?YesNoUse KhaltiTarget audience rural/national?YesNoUse eSewaUse KhaltiBest Practice: Support BothMaximize conversion across all Nepali demographics
Decision framework for selecting Nepal payment gateways based on technical requirements and target market

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 9800000000 trigger specific scenarios (success, insufficient balance, timeout). Always test the full initiate → verify cycle, not just initiation.
  • eSewa UAT: Access via uat.esewa.com.np with 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 PaymentGatewayInterface that 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.

  1. Credentials isolated per environment: Production and sandbox keys stored separately in .env.production and .env.staging. No hardcoded values anywhere in codebase.
  2. Amount verification enforced: Every successful callback or lookup compares gateway-reported amount against stored order amount before fulfillment.
  3. Idempotent processing: Duplicate callbacks (common with eSewa) handled gracefully using database unique constraints on transaction IDs. Orders updated only once.
  4. Logging comprehensive but safe: All gateway interactions logged with request/response metadata. Sensitive data (full phone numbers, tokens) masked or excluded.
  5. Timeouts and retries configured: HTTP clients have explicit timeouts (10s recommended). Retries limited to idempotent operations only—never retry payment initiation blindly.
  6. Error messages user-friendly: Frontend displays actionable messages (“Payment incomplete, please try again”) instead of raw API errors or stack traces.
  7. Database transactions used: Order status updates and fulfillment actions wrapped in DB transactions to prevent partial states if verification succeeds but save fails.
  8. 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.

Frequently Asked Questions

No. Neither gateway provides an official Laravel package. Use direct HTTP requests via Laravel's Http facade to their REST APIs.

Khalti charges 1.5% plus NPR 10 per transaction. eSewa typically charges 1.5% to 2% depending on merchant category and volume.

Yes. Both provide sandbox environments with test credentials and dummy cards for development before going live.

Khalti uses a straightforward server-side verification API after client-side payment completion. eSewa requires HMAC-SHA256 signature generation for both request initiation and callback verification, making its implementation more cryptographically complex. In my experience building Nepal Gift Card, Khalti was faster to integrate initially, but eSewa's stricter signing provides better tamper protection. Always validate responses server-side regardless of gateway, as client-side JavaScript can be manipulated by users attempting to bypass payment flows.

Never trust client-side success callbacks alone. After receiving a pidx or transaction ID from your frontend, make a server-to-server GET request to Khalti's verification endpoint using your secret key. Compare the returned amount and status against your stored order record. Only mark orders as paid if amounts match exactly and status is Completed. On production Laravel applications I maintain, skipping this step has led to fraudulent order confirmations where users modified JavaScript responses to fake successful payments without actual fund transfers.

Signature mismatches usually stem from incorrect parameter ordering, missing fields, or encoding differences. eSewa requires alphabetically sorted parameters concatenated with specific delimiters before HMAC-SHA256 hashing. Ensure you are using the exact secret key from the merchant dashboard, not the test key. Verify that no extra whitespace or URL encoding exists in the signed string. In practice, logging the exact string being hashed and comparing it byte-for-byte against eSewa documentation examples resolves most issues within minutes rather than hours of guessing.

Store only non-sensitive identifiers like merchant IDs in .env. Keep secret keys and API tokens in encrypted configuration or a secrets manager, especially on shared hosting. Never commit .env to version control. For projects I deploy via Deployer 7, sensitive keys live in shared release directories outside the repository. Rotate keys immediately if exposed. Both Khalti and eSewa support multiple API keys, so use separate credentials for development, staging, and production environments to limit blast radius during testing accidents.

Implement idempotent webhook handlers that check transaction state before processing. Store raw payloads in a dedicated table with processing status. Retry failed webhooks using Laravel queues with exponential backoff. Log all incoming requests for debugging. On legal-tech portals I have built, payment confirmations sometimes arrive out of order or duplicated due to network issues. Without idempotency checks, duplicate webhooks created double bookings or sent multiple confirmation emails. Always verify transaction uniqueness using gateway-provided reference IDs before updating business records.

Create a dedicated payments table storing gateway name, transaction ID, pidx, amount, currency, status, raw response JSON, and foreign key to orders. Never overwrite existing records; insert new rows for each status change to maintain audit trails. Add indexes on transaction_id and order_id for fast lookups during verification. Include nullable fields for refund data and dispute notes. This structure supports reconciliation, debugging, and compliance reporting. On eCommerce systems handling NPR transactions, this pattern has proven essential for resolving customer disputes and matching gateway settlement reports against application records accurately.

Define a PaymentGateway interface with methods for initiate, verify, and refund. Create separate KhaltiGateway and EsewaGateway classes implementing this contract. Bind the appropriate implementation via Laravel service container based on configuration. Controllers and services depend only on the interface. This allows switching gateways or adding new ones like IME Pay without modifying business logic. I use this pattern on multi-gateway projects where clients want fallback options. It also simplifies testing by allowing mock implementations during development without hitting live APIs repeatedly.

Khalti and eSewa handle card data entirely on their side via hosted checkout or embedded widgets, so your Laravel app never touches raw card numbers. This significantly reduces PCI scope. However, you must still secure transmitted tokens, protect API keys, and ensure HTTPS everywhere. Do not log sensitive payment data. On client projects, I configure Content Security Policy headers and enforce TLS 1.2 minimum. While full PCI DSS certification may not be required for small merchants, following these practices prevents data breaches and builds customer trust in Nepal's growing digital payment ecosystem.

Invalid request errors typically indicate malformed JSON, missing required fields, or incorrect content-type headers. Verify your payload matches Khalti's current API specification exactly, including nested object structures. Test requests using curl or Postman first to isolate whether the issue is Laravel-specific or API-related. Check that your sandbox merchant ID and secret key pair are correctly configured and not swapped with production credentials. In my experience, Khalti occasionally updates sandbox endpoints without notice, so confirming base URLs against latest documentation prevents wasted debugging time on otherwise correct implementations.

eSewa does not guarantee immediate failure notification. Your application should treat initiated-but-unverified payments as pending indefinitely until verified via server-side polling or eventual webhook. Implement a scheduled command that checks pending transactions older than fifteen minutes against eSewa's status API. Auto-cancel unconfirmed orders after a configurable timeout to release inventory. On WooCommerce and custom Laravel carts I have maintained, abandoned payment sessions caused stock discrepancies and frustrated customers expecting instant confirmation. Proactive reconciliation prevents orphaned orders and ensures accurate financial reporting.

No. Both gateways process only NPR transactions from Nepali bank accounts, wallets, or cards. International customers cannot pay directly. For cross-border sales, integrate Stripe or PayPal alongside local options. On Petals Qatar, which serves international customers while sourcing from Nepal, we used Stripe for USD payments and eSewa for domestic NPR settlements. Display currency conversion clearly at checkout. Attempting to force international cards through Nepali gateways results in declined transactions and poor user experience. Choose gateways based on actual customer location and banking capabilities, not just developer convenience.

Map existing transaction records to Laravel's payment model first. Preserve original gateway reference IDs to maintain continuity for refunds and support queries. Implement dual-write during transition period where both old and new systems record payments temporarily. Redirect legacy callback URLs to Laravel routes using server rewrites until DNS propagation completes. Validate migrated data against gateway settlement reports before decommissioning old code. On legal-tech platforms upgraded from core PHP, incremental migration prevented revenue loss during cutover. Never delete historical payment data; archive it for audit purposes even after full migration completes successfully.

Share this article

Quick Contact Options
Choose how you want to connect me: