
August 16, 2026
10 min read
Table of Contents
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.
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.
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.
| Pitfall | Symptom | Solution |
|---|---|---|
| Amount unit confusion | Customer charged Rs 10 instead of Rs 1,000 | Always multiply NPR by 100 and cast to int before sending |
| Missing purchase_order_id | Cannot reconcile payments in Khalti dashboard | Pass unique order ID in initiation; store pidx in DB |
| Trusting client callback | Fraudulent orders marked as paid | Always verify via /lookup/ endpoint server-side |
| Webhook signature ignored | Fake payments accepted from external requests | Validate HMAC-SHA256 signature on every webhook |
| Timeout too short | Intermittent failures during peak hours | Set HTTP timeout ≥30s; use retry logic with backoff |
| No idempotency check | Duplicate order updates on webhook retry | Check 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.
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:
- Test with real cards: Use Khalti’s test wallet numbers to simulate successful, failed, and expired payments. Verify each path updates your database correctly.
- Monitor webhook delivery: Set up logging for all incoming webhooks. Alert on signature mismatches or repeated failures.
- Implement retry logic: Network issues between Nepal servers and Khalti API happen. Wrap verification calls in retry blocks with exponential backoff.
- Reconcile daily: Build an artisan command that compares your paid orders against Khalti’s settlement report. Catch discrepancies before customers complain.
- 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.

