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.
Laravel 10, or newer
Composer
PhonePe Merchant Account (with test credentials)
Basic knowledge of routes, controllers, and middleware
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
Go to PhonePe Developer Portal.
Sign up for a merchant account.
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
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
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'
];
}
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.');
}
}
}
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
You have successfully integrated Phonepe Payment Gateway with Laravel! 🎉