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.

Khalti Integration Guide for Laravel Apps

By Kokil Thapa | Last reviewed: August 2026

Integrating local payment gateways is often the most critical step for any Nepali eCommerce platform or service portal. This Khalti Integration Guide for Laravel Apps provides a production-ready implementation strategy using the current API v2 endpoints, moving beyond basic tutorials to address real-world verification failures and webhook security. Whether you are building a legal-tech portal or an online store, getting this integration right prevents revenue leakage and builds user trust. For broader context on handling multiple gateways, see my overview of Laravel payment integrations.

How do you configure environment variables for Khalti Integration Guide for Laravel Apps?

Before writing a single line of PHP, you must establish a secure configuration foundation. In my experience maintaining platforms like Nepal Gift Card and various legal service portals, hardcoding keys or storing them in the wrong place is the most common security vulnerability in Nepal-based projects. Laravel 12 uses the standard .env file for secrets, but Khalti requires specific attention to key formatting and environment separation.

Khalti provides two distinct sets of credentials: one for the test environment (sandbox) and one for production. The test gateway lives at https://dev.khalti.com, while production uses https://khalti.com. Mixing these up causes silent failures where transactions appear successful locally but fail in live environments. Your .env should explicitly define both the base URL and the secret key:

<!-- .env -->
KHALTI_GATEWAY_URL=https://dev.khalti.com
KHALTI_SECRET_KEY=test_secret_key_abc123xyz
KHALTI_WEBHOOK_SECRET=whsec_test_signature_key

<!-- Production .env -->
KHALTI_GATEWAY_URL=https://khalti.com
KHALTI_SECRET_KEY=live_secret_key_prod_xyz789
KHALTI_WEBHOOK_SECRET=whsec_live_signature_key

Create a dedicated config file at config/khalti.php to expose these values safely throughout your application. This follows Laravel best practices and allows you to cache configuration in production:

<?php
// config/khalti.php
return [
    'gateway_url' => env('KHALTI_GATEWAY_URL', 'https://dev.khalti.com'),
    'secret_key'  => env('KHALTI_SECRET_KEY', ''),
    'webhook_secret' => env('KHALTI_WEBHOOK_SECRET', ''),
    'timeout'     => env('KHALTI_TIMEOUT', 30),
];

A frequent mistake I encounter during audits is developers committing the .env file to Git or using the same key across staging and production. Always verify your .gitignore includes .env. For teams working on projects like those listed in my development services, I recommend rotating keys quarterly and immediately after any developer offboarding.

Development Envdev.khalti.comtest_secret_key_*Sandbox TestingProduction Envkhalti.comlive_secret_key_*Real TransactionsLaravel Configconfig/khalti.phpReads from .envCached in ProdNever commit .env • Rotate keys quarterly • Separate test/live credentials
Secure credential management separates development and production environments to prevent accidental live charges during testing.

How do you initiate and verify payments in Khalti Integration Guide for Laravel Apps?

The core of any Khalti integration involves two distinct API calls: initiation and verification. Many developers stop at initiation because the redirect feels complete, but skipping server-side verification is how you lose money. Fraudulent users can manipulate JavaScript callbacks or forge POST data. Your Laravel backend must independently confirm every transaction.

Step 1: Initialize the Payment

Use the /api/v2/epayment/initiate/ endpoint to create a payment request. This returns a payment_url that redirects the user to Khalti’s hosted checkout. Store the returned pidx (payment index) in your database linked to the order before redirecting:

<?php
// app/Services/KhaltiService.php
use Illuminate\Support\Facades\Http;

public function initiatePayment(float $amount, string $orderId, string $returnUrl): array
{
    $response = Http::withHeaders([
        'Authorization' => 'Key ' . config('khalti.secret_key'),
        'Content-Type'  => 'application/json',
    ])->timeout(config('khalti.timeout'))
      ->post(config('khalti.gateway_url') . '/api/v2/epayment/initiate/', [
          'amount'              => (int) ($amount * 100), // Paisa conversion
          'purchase_order_id'   => $orderId,
          'purchase_order_name' => "Order #{$orderId}",
          'return_url'          => $returnUrl,
          'website_url'         => config('app.url'),
      ]);

    if (!$response->successful()) {
        throw new \Exception('Khalti initiation failed: ' . $response->body());
    }

    return $response->json();
}

Note the amount conversion: Khalti expects amounts in paisa (1 NPR = 100 paisa). Passing 1000 instead of 100000 for Rs 1,000 is a classic bug that results in charging customers Rs 10 instead of Rs 1,000. Always cast to integer after multiplication to avoid floating-point precision errors.

Step 2: Server-Side Verification

After the user completes payment, Khalti redirects to your return_url with query parameters including pidx. Do not update order status based solely on this redirect. Instead, call the lookup endpoint:

public function verifyPayment(string $pidx): array
{
    $response = Http::withHeaders([
        'Authorization' => 'Key ' . config('khalti.secret_key'),
    ])->get(config('khalti.gateway_url') . '/api/v2/epayment/lookup/', [
        'pidx' => $pidx,
    ]);

    $data = $response->json();
    
    // Only accept completed status
    if (($data['status'] ?? '') !== 'Completed') {
        throw new \Exception("Payment not completed. Status: {$data['status']}");
    }

    return $data;
}

This verification step is non-negotiable. I’ve audited systems where attackers modified the success callback URL parameters to mark unpaid orders as paid. The lookup API is your single source of truth. For more on structuring these service classes cleanly, review Laravel API best practices.

User BrowserLaravel AppDatabaseKhalti API1. Click Pay2. Create pending order3. POST /initiate/4. Return pidx + URL5. Redirect to Khalti6. Return with pidx7. GET /lookup/ (MANDATORY)8. Confirm Completed9. Update order → Paid
Server-side verification via /lookup/ endpoint is mandatory before updating order status to prevent fraud.

How do you handle webhooks securely in Khalti Integration Guide for Laravel Apps?

Webhooks provide asynchronous confirmation when payments complete, especially useful for slow networks or when users close the browser mid-redirect. However, webhooks are publicly accessible endpoints that anyone can call. Without proper validation, an attacker could send fake webhook payloads to mark orders as paid without actual payment.

Khalti signs webhook payloads using HMAC-SHA256. Your Laravel route must verify this signature before processing:

<?php
// app/Http/Controllers/Webhook/KhaltiWebhookController.php
namespace App\Http\Controllers\Webhook;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;

class KhaltiWebhookController extends Controller
{
    public function __invoke(Request $request)
    {
        $signature = $request->header('X-Khalti-Signature');
        $payload   = $request->getContent();
        $secret    = config('khalti.webhook_secret');

        $expectedSignature = hash_hmac('sha256', $payload, $secret);

        if (!hash_equals($expectedSignature, $signature)) {
            Log::warning('Khalti webhook signature mismatch', [
                'ip' => $request->ip(),
            ]);
            abort(403, 'Invalid signature');
        }

        $event = json_decode($payload, true);
        
        // Process only completed payments
        if (($event['status'] ?? '') === 'Completed') {
            // Dispatch job to update order
            // Use pidx to find order, verify amount matches
        }

        return response()->json(['message' => 'OK'], 200);
    }
}

Always use hash_equals() instead of === for signature comparison to prevent timing attacks. Register this route without CSRF middleware since Khalti won’t include a token:

// routes/web.php or routes/api.php
Route::post('/webhooks/khalti', Webhook\KhaltiWebhookController::class)
    ->withoutMiddleware([\App\Http\Middleware\VerifyCsrfToken::class]);

In production deployments on Ubuntu servers running PHP-FPM, ensure your webhook endpoint responds within 30 seconds. If order processing takes longer, dispatch a queued job and return 200 immediately. Khalti retries failed webhooks with exponential backoff, but consistent timeouts will eventually stop delivery.

What are common pitfalls in Khalti Integration Guide for Laravel Apps?

After integrating Khalti across multiple client projects—from digital gift card platforms to legal service portals—I’ve documented recurring issues that don’t appear in official documentation. Avoiding these saves days of debugging.

PitfallSymptomSolution
Amount unit confusionCustomer charged Rs 10 instead of Rs 1,000Always multiply NPR by 100 and cast to int before sending
Missing purchase_order_idCannot reconcile payments in Khalti dashboardPass unique order ID in initiation; store pidx in DB
Trusting client callbackFraudulent orders marked as paidAlways verify via /lookup/ endpoint server-side
Webhook signature ignoredFake payments accepted from external requestsValidate HMAC-SHA256 signature on every webhook
Timeout too shortIntermittent failures during peak hoursSet HTTP timeout ≥30s; use retry logic with backoff
No idempotency checkDuplicate order updates on webhook retryCheck current order status before updating; use DB locks

Another subtle issue occurs when deploying behind load balancers or reverse proxies. If your return_url uses HTTP instead of HTTPS, Khalti may reject the initiation request. Always generate URLs using url()->secure() or ensure APP_URL starts with https://. On shared hosting environments common in Nepal, verify that outgoing HTTPS requests aren’t blocked by firewall rules—some providers restrict ports other than 80/443.

Receive pidx from RedirectCall /lookup/ APIStatus ≠ CompletedShow error / RetryStatus = CompletedVerify amount matchesAPI Error / TimeoutLog + Queue retryUpdate Order → PaidNever fulfill order without Completed status + amount match
Verification decision tree ensures only fully confirmed payments trigger order fulfillment.

How does Khalti compare to eSewa for Laravel Integration Guide for Laravel Apps?

Most Nepal-facing applications need both Khalti and eSewa. Understanding their differences helps you architect a unified payment service rather than duplicating logic. While this guide focuses on Khalti, here’s how they differ in practice for Laravel developers:

  • API Design: Khalti uses RESTful JSON endpoints with Bearer-style auth headers. eSewa traditionally relied on form posts and XML responses, though newer versions support JSON. Khalti’s API is generally easier to integrate with Laravel’s HTTP client.
  • Verification Flow: Both require server-side verification, but Khalti’s /lookup/ endpoint is synchronous and immediate. eSewa verification sometimes involves parsing HTML responses or dealing with inconsistent field names across API versions.
  • Webhook Support: Khalti provides signed webhooks out of the box. eSewa webhook support varies by merchant tier and often requires manual activation through their support team.
  • Sandbox Quality: Khalti’s test environment closely mirrors production with realistic error codes. eSewa sandbox occasionally behaves differently than live, requiring extra caution during UAT.
  • Documentation: Khalti maintains updated docs with cURL examples that translate directly to Laravel HTTP calls. eSewa documentation can be outdated, leading to trial-and-error integration.

For projects serving diverse Nepali customers, I typically implement a payment gateway interface that abstracts these differences. This lets controllers remain agnostic while swapping implementations based on user choice. See my comparison of Khalti and eSewa integration for detailed code patterns.

Final Steps for Production Khalti Integration Guide for Laravel Apps

Deploying Khalti integration to production requires more than switching environment variables. Run through this checklist before going live:

  1. Test with real cards: Use Khalti’s test wallet numbers to simulate successful, failed, and expired payments. Verify each path updates your database correctly.
  2. Monitor webhook delivery: Set up logging for all incoming webhooks. Alert on signature mismatches or repeated failures.
  3. Implement retry logic: Network issues between Nepal servers and Khalti API happen. Wrap verification calls in retry blocks with exponential backoff.
  4. Reconcile daily: Build an artisan command that compares your paid orders against Khalti’s settlement report. Catch discrepancies before customers complain.
  5. Document your flow: Future maintainers need to understand why verification exists and what happens when it fails. Add inline comments explaining security decisions.

This Khalti Integration Guide for Laravel Apps covers the technical foundation, but payment systems evolve. Regularly review Khalti’s changelog for deprecations or new features. If you’re building a Nepal-focused platform and need hands-on implementation support, reach out to discuss your project. Getting payments right is non-negotiable for user trust and business viability.

Frequently Asked Questions

No, Khalti does not maintain an official Laravel SDK. In my experience building payment flows for Nepal Gift Card and other client projects, you should use the official REST API directly via Laravel's HTTP Client or Guzzle. This avoids dependency on unmaintained third-party packages that often lag behind API changes and lack support for newer verification endpoints required in production environments.

Standard merchant rate is 1.5% plus NPR 10 per successful transaction.

Test keys work only against sandbox.khalti.com and accept dummy credentials for development. Live keys connect to khalti.com and process real NPR transactions. Never commit live secret keys to version control; store them in your .env file and validate environment-specific configuration during deployment to prevent accidental production charges during staging tests.

Always verify payments server-side using the verification endpoint at api.khalti.com/api/v2/epayment/lookup/. Pass the pidx received from the frontend redirect along with your live secret key. Never trust client-side success callbacks alone, as these can be manipulated. In production Laravel applications, I implement this check inside a dedicated service class before updating order status or delivering digital goods.

Yes, using the Khalti Web Checkout widget embedded via JavaScript. The widget handles the payment UI in a modal overlay while keeping users on your domain. However, server-side verification remains mandatory after the widget returns a success token. On eCommerce projects like Petals Nepal, this approach reduced cart abandonment compared to full-page redirects while maintaining security through backend validation.

Typically three to five business days after submitting complete KYC documents.

Implement retry logic with exponential backoff using Laravel's HTTP Client retry method, capped at three attempts. If verification fails repeatedly, mark the order as pending and queue a background job to recheck later. Display a clear message to users rather than generic errors. On legal-tech portals handling service payments, this pattern prevents lost bookings during intermittent gateway outages common with Nepali payment providers.

Yes, Khalti requires HTTPS for all live API calls and webhook callbacks. Let's Encrypt certificates via Certbot are sufficient and free. Without valid SSL, the payment widget will fail to load and API requests will be rejected. Ensure your Laravel APP_URL matches the certificate domain exactly, including www prefix consistency, to avoid mixed-content warnings that break the checkout flow.

Store the pidx or transaction_id in your database with a unique constraint. Before processing any payment confirmation, check if that identifier already exists. Use database transactions to ensure atomicity between recording the payment and updating order status. This idempotency pattern prevents double-charging or duplicate service activations, which I have encountered on subscription platforms where network retries caused multiple callback deliveries for single transactions.

Both cover similar user bases, but Khalti offers cleaner API documentation and faster sandbox testing. eSewa has broader rural reach and bank transfer options. Many Nepal-facing stores integrate both. Choose based on your customer demographics rather than technical superiority. For urban digital products like Nepal Gift Card, Khalti alone sufficed; for florist delivery services targeting diverse regions, dual integration proved necessary.

Amounts must be sent in paisa as integers, not decimal rupees.

Use Laravel's Log facade within your payment service to record request payloads, response codes, and verification results. Structure logs with context arrays containing order IDs and pidx values for traceability. Avoid logging raw secret keys or sensitive customer data. On production systems, I configure separate log channels for payment events to isolate them from application noise, making troubleshooting failed transactions significantly faster during support incidents.

Only if they possess a Khalti wallet, which requires a Nepali mobile number and KYC. International cards cannot fund Khalti wallets directly. For stores serving diaspora customers, such as Quick And Easy Nepalese Grocery targeting Australian Nepalis, you must offer Stripe or PayPal alongside Khalti. Clearly indicate payment method restrictions on the checkout page to prevent confusion and abandoned carts from users expecting global card acceptance.

Use ngrok or Cloudflare Tunnel to expose your local Laravel server to the internet. Configure the tunnel URL as your webhook endpoint in the Khalti dashboard sandbox settings. Verify that your route accepts POST requests and returns appropriate HTTP status codes. Remember that webhook signatures must be validated using your test secret key. This setup allows end-to-end testing of asynchronous payment confirmations without deploying to staging servers.

Most failures stem from using test keys against live endpoints, sending amounts in rupees instead of paisa, or mismatched environment variables after deployment. Another frequent issue is incorrect Content-Type headers when calling the lookup endpoint. Always verify your .env contains the correct key pair for the target environment. Run artisan config:cache after deployments to prevent stale cached values from overriding updated credentials in production releases.

Share this article

Quick Contact Options
Choose how you want to connect me: