Skip to content
Payments⏱️ 13 min read·

eSewa Payment Integration in Laravel 12: Complete Step-by-Step Guide with Signature Verification

Learn how to integrate eSewa ePay v2 in Laravel 12 from scratch. Includes HMAC-SHA256 signature generation, sandbox credentials, controller implementation, CSRF exemption in bootstrap/app.php, and server-side status verification.

Need this wired into a product? See payment gateway integration.

MN
Manoj NeupaneSoftware Developer & Programming Instructor
#eSewa#Laravel 12#PHP#Payment Gateway#HMAC-SHA256#ePay v2
🎯 Who This Tutorial Is For

📋 Prerequisites
  • ✓A working Laravel 11/12 application running PHP 8.2 or higher
  • ✓MySQL database connected and migrations executed
  • ✓Basic understanding of Blade views, Controllers, and Routes
  • ✓eSewa sandbox test credentials from developer.esewa.com.np

🚀 What You Will Learn & Build

  • ▸How to configure eSewa sandbox environment variables in Laravel .env
  • ▸Generating the HMAC-SHA256 signature required by eSewa ePay v2
  • ▸Bypassing CSRF verification for eSewa POST callbacks in Laravel 11/12
  • ▸Verifying transaction status securely using Laravel Http client
  • ▸Preventing fraudulent order tampering and duplicate payments
1

1. Understanding eSewa ePay v2 Flow in Laravel 12

In eSewa's modern **ePay v2** protocol, the integration operates as follows: 1. Customer clicks "Pay with eSewa" in your checkout view. 2. Your Laravel controller creates a pending `Order` in the database with a unique `transaction_uuid` and exact amount. 3. Your controller computes an **HMAC-SHA256** hash using the secret key (`8gBm/:&EnhH.1/q` in sandbox) over the concatenated fields: `total_amount,transaction_uuid,product_code`. 4. A frontend HTML form automatically submits these parameters to eSewa's gateway endpoint (`https://rc-epay.esewa.com.np/api/epay/main/v2/form`). 5. After the user logs in and approves payment on eSewa, eSewa redirects to your registered `success_url` with an encoded base64 payload. 6. Your backend server decodes the payload, validates the return signature, and makes a server-to-server call to verify the transaction status before marking the order as paid.
2

2. Setting Up Environment Variables (.env)

Add the official eSewa sandbox test credentials to your `.env` file. In Laravel 12, never hardcode these secrets into controllers:
.env
# eSewa v2 Sandbox Credentials (Official Test Keys)
ESEWA_PRODUCT_CODE=EPAYTEST
ESEWA_SECRET_KEY=8gBm/:&EnhH.1/q
ESEWA_GATEWAY_URL=https://rc-epay.esewa.com.np/api/epay/main/v2/form
ESEWA_VERIFICATION_URL=https://rc-epay.esewa.com.np/api/epay/transaction/status/
ESEWA_SUCCESS_URL=http://localhost:8000/payment/esewa/success
ESEWA_FAILURE_URL=http://localhost:8000/payment/esewa/failure
3

3. Creating the eSewa Payment Controller

Create `app/Http/Controllers/EsewaPaymentController.php` to handle payment initiation, signature generation, and verification:
app/Http/Controllers/EsewaPaymentController.php
<?php

namespace AppHttpControllers;

use IlluminateHttpRequest;
use IlluminateSupportFacadesHttp;
use IlluminateSupportStr;

class EsewaPaymentController extends Controller
{
    /**
     * Initiate checkout: generate signature and redirect to eSewa
     */
    public function initiatePayment(Request $request)
    {
        $amount = 100.00; // Total amount in NPR
        $taxAmount = 0.00;
        $totalAmount = $amount + $taxAmount;
        $transactionUuid = Str::uuid()->toString();
        $productCode = config('services.esewa.product_code', env('ESEWA_PRODUCT_CODE'));
        $secretKey = config('services.esewa.secret_key', env('ESEWA_SECRET_KEY'));

        // Format: total_amount,transaction_uuid,product_code
        $dataToSign = "total_amount={$totalAmount},transaction_uuid={$transactionUuid},product_code={$productCode}";
        $signature = base64_encode(hash_hmac('sha256', $dataToSign, $secretKey, true));

        $formData = [
            'amount' => $amount,
            'tax_amount' => $taxAmount,
            'total_amount' => $totalAmount,
            'transaction_uuid' => $transactionUuid,
            'product_code' => $productCode,
            'product_service_charge' => 0,
            'product_delivery_charge' => 0,
            'success_url' => env('ESEWA_SUCCESS_URL'),
            'failure_url' => env('ESEWA_FAILURE_URL'),
            'signed_field_names' => 'total_amount,transaction_uuid,product_code',
            'signature' => $signature,
        ];

        return view('payment.esewa_redirect', [
            'gatewayUrl' => env('ESEWA_GATEWAY_URL'),
            'formData' => $formData
        ]);
    }

    /**
     * Handle payment success callback from eSewa
     */
    public function paymentSuccess(Request $request)
    {
        $encodedData = $request->query('data');
        if (!$encodedData) {
            return redirect()->route('cart')->with('error', 'Invalid callback payload from eSewa.');
        }

        // Decode base64 response from eSewa
        $decodedJson = base64_decode($encodedData);
        $response = json_decode($decodedJson, true);

        if (!$response || !isset($response['transaction_uuid'], $response['total_amount'])) {
            return redirect()->route('cart')->with('error', 'Malformed payment confirmation payload.');
        }

        // Double check status server-side
        $verifyRes = Http::get(env('ESEWA_VERIFICATION_URL'), [
            'product_code' => env('ESEWA_PRODUCT_CODE'),
            'total_amount' => $response['total_amount'],
            'transaction_uuid' => $response['transaction_uuid'],
        ]);

        if ($verifyRes->successful() && ($verifyRes->json('status') === 'COMPLETE' || $response['status'] === 'COMPLETE')) {
            // Fulfill order in database atomically
            // Order::where('transaction_uuid', $response['transaction_uuid'])->update(['status' => 'PAID']);
            return view('payment.success', ['details' => $response]);
        }

        return redirect()->route('cart')->with('error', 'Payment verification failed with eSewa.');
    }

    public function paymentFailure(Request $request)
    {
        return view('payment.failure', ['message' => 'Transaction was cancelled or declined by eSewa.']);
    }
}
4

4. Exclude Payment Callback from CSRF Protection in Laravel 12

In Laravel 11 and Laravel 12, application middleware is configured directly in `bootstrap/app.php` instead of the legacy `app/Http/Kernel.php`. Because eSewa redirects externally, any POST callback will fail with **419 Page Expired** unless excluded:
bootstrap/app.php
<?php

use IlluminateFoundationApplication;
use IlluminateFoundationConfigurationExceptions;
use IlluminateFoundationConfigurationMiddleware;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {
        // Exclude eSewa and Khalti callback routes from CSRF verification
        $middleware->validateCsrfTokens(except: [
            'payment/esewa/*',
            'payment/khalti/*',
        ]);
    })
    ->withExceptions(function (Exceptions $exceptions) {
        //
    })->create();

✨ Expected Output & Verification

A frictionless checkout experience that redirects users to eSewa's test wallet, processes test payment, and redirects back with cryptographic proof and server status confirmation.

🧪 How to Test the Implementation

  1. 1Start your local server: 'php artisan serve'
  2. 2Trigger the checkout route in your browser: http://localhost:8000/payment/esewa
  3. 3Log into eSewa test sandbox using mobile '9841000000' and MPIN '1122'
  4. 4Enter test OTP '123456' to confirm transaction
  5. 5Verify that redirect lands on /payment/esewa/success with status COMPLETE

⚠️ Common Errors & How to Fix Them

❌Invalid Signature Error on eSewa redirect

Root Cause: Concatenation string does not match signed_field_names exactly, or floating points are formatted with trailing spaces.

Fix: Format amount strictly with number_format($amt, 2, '.', '') without thousands commas.

❌419 Page Expired on Callback

Root Cause: Laravel CSRF middleware rejecting incoming POST callbacks from eSewa servers.

Fix: Add 'payment/esewa/*' to validateCsrfTokens(except: [...]) in bootstrap/app.php.

🛡️ Security Considerations & Best Practices

  • !Always query eSewa verification endpoint with your merchant credentials before updating order status.
  • !Check that response['total_amount'] matches the recorded order amount to prevent client-side cart tampering.

❓ Frequently Asked Questions (FAQ)

Test eSewa ID: 9841000000, MPIN: 1122, Token/OTP: 123456. Product code is EPAYTEST.

📌 Summary of Key Takeaways

Integrating eSewa ePay v2 in Laravel 12 requires precise HMAC-SHA256 hash generation, bootstrap/app.php CSRF exemption, and server-side HTTP status verification.

Official References & Further Reading