Get Appointment

Blog Single

Laravel PhonePe Payment Gateway Integration Step-by-Step Guide

  • Vfix Technology
  • 15 May 2025
  • Laravel
  • 91 Views

Integrating PhonePe into your Laravel-based e-commerce or service platform ensures secure, fast, and seamless UPI transactions for your users. In this blog, we’ll guide you through a step-by-step Laravel PhonePe integration, from setting up your account to verifying transaction responses.
 

✅ Prerequisites

  • Laravel 10, or newer

  • Composer

  • PhonePe Merchant Account (with test credentials)

  • Basic knowledge of routes, controllers, and middleware
     

Step 1: Install Laravel

If you haven’t already set up a Laravel project, install Laravel using Composer:

composer create-project --prefer-dist laravel/laravel phonepe-laravel

Navigate to the project directory:

cd phonepe-laravel

Step 2: Create a PhonePe Merchant Account

  1. Go to PhonePe Developer Portal.

  2. Sign up for a merchant account.

  3. Collect your Merchant ID, Salt Key, and Salt Index.

Get your API keys from Dashboard. Add them to your .env file:

PHONEPE_MERCHANT_ID=**********
PHONEPE_API_KEY=**************************
PHONEPE_ENV='sandbox'  # or 'production'
PHONEPE_SALT_INDEX=1

Step 3: Create the Payments Migration:

Generate the migration file:

php artisan make:migration create_payments_table

Edit the generated file in:
database/migrations/xxxx_xx_xx_xxxxxx_create_payments_table.php:
 

public function up()
{
    Schema::create('payments', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->string('email');
        $table->string('phone');
        $table->decimal('amount', 10, 2);
        $table->string('payment_id')->nullable();
        $table->string('order_id')->nullable();
        $table->boolean('status')->default(0); // 0 = Pending, 1 = Success
        $table->json('other')->nullable();
        $table->timestamps();
    });
}

Run the migration:
 

php artisan migrate

Step 4: Create the Payment Model

Run the following command:

php artisan make:model Payment

Edit app/Models/Payment.php:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Payment extends Model
{
    use HasFactory;

    protected $fillable = ['name', 'email', 'phone', 'amount', 'order_id', 'payment_id', 'status','other'];

    protected $casts = [
        'other' => 'array'
    ];
}

Step 5: Create the Phonepe Controller:

php artisan make:controller PhonepeController

Edit app/Http/Controllers/PhonepeController.php:
 

<?php

namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Payment;
use Illuminate\Support\Facades\Http;


class PhonepeController extends Controller
{

   public function index()
   {
        return view('index');
   }

    public function payment(Request $request)
    {
        $request->validate([
            'name'   => 'required|string|max:255',
            'email'  => 'required|email',
            'phone'  => 'required|string|max:20',
            'amount' => 'required|numeric',
        ]);

        $amount = $request->input('amount');
        $name   = $request->input('name');
        $email  = $request->input('email');
        $phone  = $request->input('phone');

        // Get PhonePe credentials from .env
        $merchantId   = env('PHONEPE_MERCHANT_ID');
        $apiKey       = env('PHONEPE_API_KEY');
        $environment  = env('PHONEPE_ENV');
        $salt_index   = env('PHONEPE_SALT_INDEX', 1);
        $redirectUrl  = route('phonepe.success');

        // Unique Order ID
        $order_id = uniqid();

        // Prepare transaction data
        $transaction_data = [
            'merchantId'            => $merchantId,
            'merchantTransactionId' => $order_id,
            'merchantUserId'        => $order_id,
            'amount'                => $amount * 100, // Convert to paise
            'redirectUrl'           => $redirectUrl,
            'redirectMode'          => "POST",
            'callbackUrl'           => $redirectUrl,
            'paymentInstrument'     => [
                'type' => "PAY_PAGE",
            ]
        ];

        $encoded      = json_encode($transaction_data);
        $payloadMain  = base64_encode($encoded);
        $payload      = $payloadMain . "/pg/v1/pay" . $apiKey;
        $sha256       = hash("sha256", $payload);
        $final_x_header = $sha256 . '###' . $salt_index;
        $json_request = json_encode(['request' => $payloadMain]);

        // Choose endpoint based on environment
        $url = $environment === 'production'
            ? "https://api.phonepe.com/apis/hermes/pg/v1/pay"
            : "https://api-preprod.phonepe.com/apis/pg-sandbox/pg/v1/pay";

        // cURL call
        $curl = curl_init();
        curl_setopt_array($curl, [
            CURLOPT_URL            => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_ENCODING       => "",
            CURLOPT_MAXREDIRS      => 10,
            CURLOPT_TIMEOUT        => 30,
            CURLOPT_HTTP_VERSION   => CURL_HTTP_VERSION_1_1,
            CURLOPT_CUSTOMREQUEST  => "POST",
            CURLOPT_POSTFIELDS     => $json_request,
            CURLOPT_HTTPHEADER     => [
                "Content-Type: application/json",
                "X-VERIFY: " . $final_x_header,
                "accept: application/json"
            ],
        ]);

        $response = curl_exec($curl);
        $err = curl_error($curl);
        curl_close($curl);

        if ($err) {
            return response()->json(['error' => 'cURL Error: ' . $err], 500);
        }

        $res = json_decode($response);

        // Save to DB
        Payment::create([
            'name'       => $name,
            'email'      => $email,
            'phone'      => $phone,
            'amount'     => $amount,
            'order_id'   => $order_id,
            'payment_id' => $res->data->transactionId ?? null,
            'status'     => 0, // pending
            'other'      => $res,
        ]);

        if (isset($res->code) && $res->code === 'PAYMENT_INITIATED') {
            $payUrl = $res->data->instrumentResponse->redirectInfo->url;
            return redirect()->away($payUrl);
        }

        return response()->json(['error' => 'Transaction Error', 'details' => $res], 400);
    }


    public function success(Request $request)
    {
        try {
            \Log::info('PhonePe Verify Incoming Request:', $request->all());

            // Get the transaction ID from callback
            $transactionId = $request->input('transactionId');

            if (!$transactionId) {
                \Log::error('PhonePe Verify: No transactionId found in callback');
                return redirect()->route('home')->with('error', 'PhonePe payment verification failed!');
            }

            // Get credentials
            $merchantId  = env('PHONEPE_MERCHANT_ID');
            $saltKey     = env('PHONEPE_API_KEY');
            $saltIndex   = env('PHONEPE_SALT_INDEX', 1);
            $environment = env('PHONEPE_ENV');

            // Construct the status check path
            $path = "/pg/v1/status/$merchantId/$transactionId";

            $baseUrl = $environment === 'production'
                ? 'https://api.phonepe.com/apis/hermes'
                : 'https://api-preprod.phonepe.com/apis/pg-sandbox';

            $statusUrl = "$baseUrl/pg/v1/status/$merchantId/$transactionId";

            // Generate checksum
            $checksum = hash('sha256', $path . $saltKey) . "###" . $saltIndex;

            \Log::info("PhonePe Status Check - URL: $statusUrl");
            \Log::info("PhonePe Status Check - Checksum: $checksum");

            $response = Http::withHeaders([
                'Content-Type' => 'application/json',
                'X-VERIFY'     => $checksum,
                'X-MERCHANT-ID' => $merchantId,
                'Accept'       => 'application/json',
            ])->get($statusUrl);

            $responseData = $response->json();
            \Log::info('PhonePe Status API Response:', $responseData);

            if (!$response->successful()) {
                \Log::error('PhonePe Status API Failed', [
                    'status' => $response->status(),
                    'response' => $response->body()
                ]);
                return redirect()->route('home')->with('error', 'PhonePe verification failed. Try again later.');
            }

            // Verify the response
            if (isset($responseData['success']) && $responseData['success'] === true) {
                $paymentData = $responseData['data'] ?? [];

                // Find payment by transactionId (stored as payment_id in your DB)
                $payment = Payment::where('order_id', $transactionId)->first();

                if ($payment) {
                    $payment->update([
                        'status' => 1,
                        'payment_id' => $responseData['data']['transactionId'] ?? null,
                        'other'  => $responseData
                    ]);

                    if ($paymentData['state'] === 'COMPLETED') {
                        return redirect()->route('home')->with([
                            'success' => 'Payment Successful!',
                            'payment_id' => $transactionId,
                            'payment' => $payment,
                        ]);
                    }
                }
            }

            \Log::error('PhonePe Verification Failed', ['response' => $responseData]);
            return redirect()->route('home')->with('error', 'Payment verification failed. Please contact support.');

        } catch (\Exception $e) {
            \Log::error('PhonePe Verify Exception: ' . $e->getMessage() . "\n" . $e->getTraceAsString());
            return redirect()->route('home')->with('error', 'Something went wrong during payment verification.');
        }
    }


}

Step 6: Define Routes

Edit routes/web.php:

<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\PhonepeController;

Route::get('/',[PhonepeController::class,'index'])->name('home');
Route::post('/phonepe/payment', [PhonepeController::class, 'payment'])->name('phonepe.payment');
Route::post('/phonepe/success', [PhonepeController::class, 'success'])->name('phonepe.success');

Step 7: Add CSRF Token expection for success route:
Edit bootstrap/app.php:

<?php

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {
        $middleware->validateCsrfTokens(except: [
        '/phonepe/success',
        ]);
    })
    ->withExceptions(function (Exceptions $exceptions) {
        //
    })->create();

Step 8: Create Views
resources/views/index.blade.php

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Cashfree Payment</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
</head>

<body>
    <section class=" pt-5">
        <div class="container">
            <div class="row justify-content-center">
                <div class="col-md-6">
                    @if (session('success'))
                        <div class="alert alert-success alert-dismissible fade show" role="alert">
                            <strong>Success!</strong> {{ session('success') }}
                            <button type="button" class="btn-close" data-bs-dismiss="alert"
                                aria-label="Close"></button>
                        </div>
                    @endif

                    @if (session('error'))
                        <div class="alert alert-danger alert-dismissible fade show" role="alert">
                            <strong>Error!</strong> {{ session('error') }}
                            <button type="button" class="btn-close" data-bs-dismiss="alert"
                                aria-label="Close"></button>
                        </div>
                    @endif

                    @if (session('payment'))
                        <div class="card my-3">
                            <div class="card-header bg-success text-white">
                                <h5 class="mb-0">Payment Details</h5>
                            </div>
                            <div class="card-body">
                                <p><strong>Payment ID:</strong> {{ session('payment')->payment_id }}</p>
                                <p><strong>Order Id:</strong> {{ session('payment')->order_id }}</p>
                                <p><strong>Amount:</strong> ₹{{ session('payment')->amount }}</p>
                            </div>
                        </div>
                    @endif

                    <div class="card p-3">
                        <h2>Phonepe Payment Integration</h2>
                        <form action="{{ route('phonepe.payment') }}" method="POST">
                            @csrf
                            <div class="mb-3">
                                <label for="name" class="form-label">Full Name</label>
                                <input type="text" name="name" value="{{ old('name') }}"
                                    class="form-control @error('name') is-invalid @enderror">
                                @error('name')
                                    <span class="text-danger">{{ $message }}</span>
                                @enderror
                            </div>

                            <div class="mb-3">
                                <label for="email" class="form-label">Email Address</label>
                                <input type="email" name="email" value="{{ old('email') }}"
                                    class="form-control @error('email') is-invalid @enderror">
                                @error('email')
                                    <span class="text-danger">{{ $message }}</span>
                                @enderror
                            </div>

                            <div class="mb-3">
                                <label for="phone" class="form-label">Phone Number</label>
                                <input type="tel" name="phone" value="{{ old('phone') }}"
                                    class="form-control @error('phone') is-invalid @enderror">
                                @error('phone')
                                    <span class="text-danger">{{ $message }}</span>
                                @enderror
                            </div>

                            <div class="mb-3">
                                <label for="amount" class="form-label">Amount (INR)</label>
                                <input type="number" name="amount" value="{{ old('amount') }}"
                                    class="form-control @error('amount') is-invalid @enderror">
                                @error('amount')
                                    <span class="text-danger">{{ $message }}</span>
                                @enderror
                            </div>

                            <button type="submit" class="btn btn-primary">Proceed to Pay</button>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </section>


 <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.min.js" integrity="sha384-VQqxDN0EQCkWoxt/0vsQvZswzTHUVOImccYmSyhJTp7kGtPed0Qcx8rK9h9YEgx+" crossorigin="anonymous"></script>

</body>

</html>

Now Run the server and it should work as it suppose to be: 

php artisan serve

Conclusion

You have successfully integrated Phonepe Payment Gateway with Laravel! 🎉

Tags
Share :


+91 8447 525 204 Request Estimate