
September 08, 2026
12 min read
By Kokil Thapa | Last reviewed: September 2026
A customer pays with Khalti or eSewa and closes the browser before your thank-you page loads. Without Webhooks for Nepal Payment Gateways Khalti eSewa, that order may stay pending forever even though money left their wallet. Redirect URLs alone are unreliable on mobile networks and shared devices common in Nepal. This guide shows how to register callbacks, verify them server-side, and mark orders paid exactly once in Laravel 13 on PHP 8.3+. It builds on the same patterns I use in Laravel Khalti and eSewa payment integration work for eCommerce and booking apps.
What are webhooks for Nepal payment gateways Khalti and eSewa?
A webhook is an HTTP request your application receives from Khalti or eSewa after a transaction changes state. The user's browser redirect is a convenience signal. The webhook is the accounting signal.
Both gateways support merchant-initiated payments where your server creates a payment session, then the customer authorises on Khalti or eSewa. When payment completes, the gateway notifies your backend. Your job is to treat that notification as untrusted input until you verify it against the gateway API.
On production Laravel apps I maintain, webhooks sit behind a dedicated route excluded from CSRF checks. They run outside the normal session middleware. They log raw payloads, verify status, then update the order inside a database transaction.
Khalti documents webhook and lookup flows in their merchant API reference. eSewa documents transaction verification through their developer portal. Always read the current docs before you ship. Gateway fields change more often than blog posts update.
How do you register and verify Khalti webhook callbacks in Laravel?
Khalti lets you set a callback URL in the merchant dashboard or pass one per payment initiation request. Use a stable production URL like https://yourdomain.com/webhooks/khalti. Localhost will not work unless you tunnel with a tool during development.
Route and middleware setup
Exclude the webhook route from CSRF verification. In Laravel 13, add the URI to bootstrap/app.php or your CSRF exception list:
/* bootstrap/app.php — exclude webhook from CSRF */
->withMiddleware(function (Middleware $middleware) {
$middleware->validateCsrfTokens(except: [
'webhooks/khalti',
'webhooks/esewa',
]);
}) Register a POST route without the auth middleware. Webhooks are authenticated by server-side lookup, not by Laravel sessions.
/* routes/web.php */
Route::post('/webhooks/khalti', [KhaltiWebhookController::class, 'handle'])
->name('webhooks.khalti'); Verification pattern
Never trust the POST body alone. Khalti provides a lookup endpoint where you pass the payment index (pidx) and your secret key. Confirm status is Completed, the amount matches your order in paisa, and the purchase_order_id maps to your internal reference.
/* app/Http/Controllers/KhaltiWebhookController.php */
public function handle(Request $request)
{
$pidx = $request->input('pidx');
if (! $pidx) {
return response('Missing pidx', 422);
}
$lookup = Http::withHeaders([
'Authorization' => 'Key '.config('services.khalti.secret_key'),
])->post('https://khalti.com/api/v2/epayment/lookup/', [
'pidx' => $pidx,
]);
if (! $lookup->successful()) {
Log::warning('Khalti lookup failed', ['pidx' => $pidx]);
return response('Lookup failed', 502);
}
$data = $lookup->json();
if (($data['status'] ?? '') !== 'Completed') {
return response('Not completed', 200);
}
$order = Order::where('payment_ref', $data['purchase_order_id'])->first();
if (! $order || (int) $order->amount_paisa !== (int) $data['total_amount']) {
Log::alert('Khalti amount mismatch', ['pidx' => $pidx]);
return response('Mismatch', 409);
}
$this->markOrderPaid($order, 'khalti', $pidx);
return response('OK', 200);
} Store Khalti keys in .env, not in Git. Use separate test and live keys. On sister sites I deploy with Deployer 7, the shared .env persists across releases while code swaps via symlink.
For a fuller initiation flow, see the dedicated Khalti integration guide for Laravel apps. That post covers session creation. This post covers what happens after the customer pays.
How should you handle eSewa IPN notifications on your server?
eSewa integrations often rely on a success URL redirect plus a server-side status check. Some merchant setups also register an IPN-style callback URL. Treat every inbound request the same way: log, verify via API, then update the order.
eSewa verification flow
When eSewa redirects the user, query parameters include oid, amt, and refId. Do not mark the order paid from those alone. Call the eSewa transaction verification endpoint with your merchant code, transaction UUID, and amount.
/* app/Services/EsewaVerificationService.php */
public function verify(string $productCode, string $txnUuid, float $amount): bool
{
$params = http_build_query([
'product_code' => $productCode,
'total_amount' => $amount,
'transaction_uuid' => $txnUuid,
]);
$response = Http::get(
config('services.esewa.verify_url').'?'.$params
);
if (! $response->successful()) {
return false;
}
$body = $response->json();
return ($body['status'] ?? '') === 'COMPLETE';
} Wire this into both the user-facing success route and a dedicated webhook route. The success page gives instant feedback. The webhook or a queued job gives reliability when the user closes the tab.
/* routes/web.php */
Route::post('/webhooks/esewa', [EsewaWebhookController::class, 'handle']);
Route::get('/payments/esewa/success', [EsewaReturnController::class, 'success']); The eSewa integration guide for PHP apps walks through form fields and test credentials. Pair that with this verification layer before you go live.
What is the difference between redirect callbacks and server webhooks?
Redirect callbacks happen in the user's browser. Server webhooks happen between data centres. Confusing the two causes the most common payment bugs I see on Nepal eCommerce sites.
| Signal type | Transport | Trust level | Typical failure |
|---|---|---|---|
| Success redirect URL | Browser GET | Low — params can be faked or lost | User closes tab before hit |
| Failure redirect URL | Browser GET | Low — same as success | False failure on slow network |
| Khalti webhook POST | Server-to-server | Medium — verify with lookup API | Wrong URL or CSRF block |
| eSewa verification API | Your server calls eSewa | High — authoritative status | Amount mismatch, wrong UUID |
| Manual admin mark-paid | Human action | High but slow | Operational bottleneck |
The redirect gives UX. The webhook plus lookup gives truth. On Quick And Easy Nepalese Grocery, a Laravel cart needed both paths because mobile data drops are common during checkout.
Compare broader gateway choices in the eCommerce payment gateway options for Nepal compared article. Khalti and eSewa dominate wallet payments, but ConnectIPS integration for bank payments follows similar webhook discipline.
How do you make Khalti and eSewa webhook handlers idempotent?
Gateways retry failed webhooks. Your handler will receive the same payment twice. Without idempotency, you ship duplicate digital goods or double-count revenue.
Database design
Add a unique index on the gateway transaction identifier. For Khalti, use pidx. For eSewa, use transaction_uuid or refId depending on your integration version.
Schema::create('payment_events', function (Blueprint $table) {
$table->id();
$table->foreignId('order_id')->constrained();
$table->string('gateway'); /* khalti | esewa */
$table->string('gateway_ref'); /* pidx or txn uuid */
$table->json('payload');
$table->timestamps();
$table->unique(['gateway', 'gateway_ref']);
}); Idempotent mark-paid logic
protected function markOrderPaid(Order $order, string $gateway, string $ref): void
{
DB::transaction(function () use ($order, $gateway, $ref) {
$exists = PaymentEvent::where('gateway', $gateway)
->where('gateway_ref', $ref)
->exists();
if ($exists) {
return; /* already processed — safe retry */
}
PaymentEvent::create([
'order_id' => $order->id,
'gateway' => $gateway,
'gateway_ref' => $ref,
'payload' => request()->all(),
]);
$order->update([
'status' => 'paid',
'paid_at' => now(),
]);
dispatch(new FulfillOrderJob($order->id));
});
} Queue fulfilment after the DB commit. Sending email or SMS inside the webhook request slows the response. Slow responses trigger more retries from the gateway.
For complex checkout flows, consider pushing webhook handling to a Laravel queue job. Return HTTP 200 quickly after persisting the raw event. Process verification asynchronously only if you can tolerate a few seconds of delay. Most Nepal merchants prefer synchronous verification for simpler ops.
When debugging payload shapes, paste anonymised JSON into the JSON formatter tool to inspect nested fields quickly.
What production mistakes break Nepal payment webhooks?
Most webhook failures I troubleshoot are infrastructure problems, not gateway bugs. Fix these before you blame Khalti or eSewa support.
- CSRF middleware blocking POST. Laravel returns 419 and the gateway logs a failure. Add webhook URIs to the CSRF exception list.
- HTTP instead of HTTPS. Both gateways require TLS in production. Let's Encrypt on Ubuntu is free and takes minutes with Certbot.
- Wrong amount units. Khalti expects paisa (Rs 500 = 50000). eSewa uses rupee decimals. A one-decimal typo fails verification every time.
- Stale opcache after deploy. Your new handler never runs. Reload PHP-FPM after symlink swap on Deployer releases.
- Missing firewall allowance. Rare, but some hosts block unknown POST sources. Khalti publishes IP ranges in their docs when applicable.
- Logging secrets. Never log full Authorization headers or secret keys. Log
pidx, order ID, and HTTP status only.
Reconciliation and monitoring
Run a nightly artisan command that finds orders stuck in pending for more than 30 minutes. Re-query the gateway lookup API for each. This catches webhooks that failed during a deploy window.
/* app/Console/Commands/ReconcilePendingPayments.php */
$pending = Order::where('status', 'pending')
->where('created_at', '<', now()->subMinutes(30))
->get();
foreach ($pending as $order) {
/* call Khalti lookup or eSewa verify */
} On legal-tech portals like Mijar Law Associates, payment webhooks trigger document access. A missed webhook means a paid client cannot download files. Reconciliation is not optional there.
Security hardening matters too. Webhook URLs are public endpoints. Rate-limit them. Reject oversized payloads. Read the guide to securing your website and server in Nepal for broader server context.
Official references stay current longer than copy-pasted URLs in forums. Consult the Khalti API documentation for webhook fields and lookup endpoints. Consult the eSewa developer portal for verification parameters. Laravel's HTTP client docs cover timeout and retry configuration for lookup calls.
If you are building a custom gateway abstraction, the payment gateway API from scratch guide explains shared interfaces. The Laravel payment integrations roundup links related patterns for Stripe and local wallets together.
For agency or in-house teams shipping checkout this quarter, API development services in Nepal and eCommerce development in Nepal cover end-to-end gateway work. Ongoing webhook monitoring fits under support and maintenance services.
The broader market context lives in the Nepal digital payment landscape 2026 overview. Wallet adoption keeps climbing, but webhook reliability separates professional stores from demo projects.
Key Takeaways
- Register public HTTPS webhook URLs for Khalti and eSewa, and exclude them from Laravel CSRF middleware.
- Always verify payments with the gateway lookup API — never trust redirect query parameters alone.
- Store gateway transaction refs with a unique database index so retries cannot double-fulfil orders.
- Match amounts in the correct unit (Khalti paisa vs eSewa rupees) before marking an order paid.
- Run nightly reconciliation on pending orders to recover from deploy windows or network blips.
- Queue fulfilment jobs after the DB commit so webhook handlers return HTTP 200 within gateway timeout limits.
People Also Ask
Does Khalti send webhooks automatically after every payment?
Khalti sends a server callback when you register a webhook URL during payment initiation or in the merchant dashboard. You still must call the lookup API to confirm status and amount. Treat the webhook as a trigger, not proof.
Can eSewa confirm payment without a user redirect?
Your server can verify any transaction by calling the eSewa status endpoint with the merchant code, UUID, and amount. That makes reconciliation possible even when the customer never returns to your site. Pair it with a success redirect for better UX.
What HTTP status should a webhook handler return?
Return 200 after the payment is persisted or safely deduplicated. Return 4xx only for malformed requests you never want retried. Return 5xx sparingly — gateways will retry, which is why idempotency matters.
Do I need webhooks if I already check payment on the success page?
Yes. Mobile users in Nepal often lose connectivity after paying. Browser redirects fail silently. Webhooks plus nightly reconciliation close the gap between money received and order marked paid.
Ship reliable Nepal payment webhooks
Webhooks for Nepal Payment Gateways Khalti eSewa are the difference between a checkout demo and a store that fulfils every paid order. Register HTTPS endpoints, verify with official lookup APIs, enforce idempotency, and reconcile pending rows nightly. That pattern works on Laravel 13 with PHP 8.3+ and scales from a Rs 2,000 gift card to multi-vendor marketplaces.
Need Khalti or eSewa wired into an existing app or a new build? Review the custom software development service and web development in Nepal offerings, then contact us with your stack and timeline.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

