yogeshgupta/phonepe-laravel
Laravel integration for PhonePe payments. Provides simple configuration and helper methods to initiate transactions, generate required hashes/signatures, and handle callbacks/responses, making it easier to accept PhonePe payments in your Laravel app.
Installation
composer require yogeshgupta/phonepe-laravel
php artisan vendor:publish --provider="Yogeshgupta\PhonepeLaravel\PhonepeServiceProvider"
Configure .env
PHONEPE_MERCHANT_ID=your_merchant_id
PHONEPE_MERCHANT_SECRET=your_merchant_secret
PHONEPE_ENV=test # or 'live'
PHONEPE_WEBHOOK_SECRET=your_webhook_secret
First Payment Flow
use Yogeshgupta\PhonepeLaravel\Facades\Phonepe;
// 1. Initiate payment (redirect user to PhonePe)
$paymentLink = Phonepe::initiatePayment([
'amount' => 100.00,
'txnId' => 'txn_' . Str::uuid(),
'callbackUrl' => route('payment.callback'),
'paymentInstrument' => [
'type' => 'UPI',
'target' => 'user@phonepe'
]
]);
return redirect()->to($paymentLink);
// 2. Handle callback (after user returns from PhonePe)
public function handleCallback(Request $request) {
$status = Phonepe::verifyPayment($request->txnId);
// Update your database and show success page
}
Webhook Setup
Add this route to your routes/web.php:
Route::post('/phonepe/webhook', [PhonepeWebhookController::class, 'handle']);
// In your checkout controller
public function checkout(Request $request) {
$payment = Phonepe::initiatePayment([
'amount' => $request->amount,
'txnId' => 'order_' . $request->order_id,
'callbackUrl' => route('payment.callback'),
'paymentInstrument' => [
'type' => 'UPI',
'target' => $request->upi_id
],
'metadata' => [
'order_id' => $request->order_id,
'customer_email' => auth()->user()->email
]
]);
return redirect()->to($payment->getRedirectUrl());
}
// PhonepeWebhookController.php
public function handle(Request $request) {
// 1. Validate webhook signature
if (!Phonepe::validateWebhook($request)) {
abort(403);
}
// 2. Process the event
$event = $request->event;
$txnId = $request->txnId;
// 3. Dispatch event or update database
event(new PhonepePaymentEvent($event, $txnId));
return response()->json(['status' => 'success']);
}
// After user returns from PhonePe
public function verifyPayment(Request $request) {
$txnId = $request->txnId;
$status = Phonepe::verifyPayment($txnId);
if ($status->getStatus() === 'PAYMENT_SUCCESS') {
// Update order status
Order::where('txn_id', $txnId)->update(['status' => 'paid']);
return view('payment.success');
}
return view('payment.failed');
}
// Custom service for refunds
public function processRefund($orderId) {
$order = Order::find($orderId);
$refund = Phonepe::refundPayment([
'txnId' => $order->txn_id,
'amount' => $order->amount,
'reason' => 'Customer requested refund'
]);
if ($refund->getStatus() === 'REFUND_SUCCESS') {
$order->update(['refund_status' => 'processed']);
}
}
// Test payment initiation
public function testPaymentInitiation() {
$mock = Mockery::mock('overload', Yogeshgupta\PhonepeLaravel\Facades\Phonepe::class);
$mock->shouldReceive('initiatePayment')
->once()
->andReturn(new PaymentResponse('https://phonepe.com/pay', 'txn_123'));
$response = $this->post('/checkout', ['amount' => 100]);
$response->assertRedirect('https://phonepe.com/pay');
}
// Test webhook validation
public function testWebhookValidation() {
$request = new Request([
'event' => 'PAYMENT_SUCCESS',
'txnId' => 'txn_123'
], [], [], [], [], [
'HTTP_X_PHONEPE_SIGNATURE' => 'valid_signature'
]);
$this->assertTrue(Phonepe::validateWebhook($request));
}
Webhook Signature Validation
if (!Phonepe::validateWebhook($request)) {
Log::warning('Invalid PhonePe webhook signature', [
'signature' => $request->header('X-PhonePe-Signature'),
'body' => $request->getContent()
]);
abort(403);
}
OAuth Token Expiry
public function handle($request, Closure $next) {
if (Phonepe::isTokenExpired()) {
Phonepe::refreshAccessToken();
}
return $next($request);
}
Idempotency Issues
Dispatch::later(now()->addMinutes(5), new ProcessPaymentWebhook($event))
->onQueue('phonepe')
->uniqueId($event->txnId);
Amount Precision
$amountInPaise = $amount * 100;
Webhook Retries
public function handle() {
try {
// Process webhook
} catch (\Exception $e) {
$this->release(60); // Retry after 1 minute
}
}
Enable Debug Logging
PHONEPE_DEBUG=true
This will log all API requests/responses to storage/logs/laravel.log.
Inspect Raw API Responses
$response = Phonepe::initiatePayment([...]);
Log::debug('PhonePe API Response', [
'status' => $response->getStatus(),
'raw' => $response->getRawContent()
]);
Test in Sandbox First
PHONEPE_ENV=test to test all flows before going live.Monitor Failed Jobs
php artisan queue:failed
Look for Yogeshgupta\PhonepeLaravel\Jobs\ProcessWebhookJob failures.
Environment-Specific Settings
PHONEPE_ENV.config/phonepe.php:
'test' => [
'merchant_id' => env('PHONEPE_TEST_MERCHANT_ID'),
'merchant_secret' => env('PHONEPE_TEST_MERCHANT_SECRET'),
'base_url' => 'https://api.phonepe.com/test',
],
Webhook Secret Management
PHONEPE_WEBHOOK_SECRET securely (use Laravel Vault or AWS Secrets Manager).Rate Limiting
$token = Cache::remember('phonepe_access_token', now()->addHours(1), function() {
return Phonepe::getAccessToken();
});
// app/Providers/PhonepeServiceProvider.php
public function boot() {
Phonepe::extend('netbanking', function($app) {
return new NetBankingPaymentService();
});
How can I help you explore Laravel packages today?