PhonePe is one of India's leading digital payment platforms that supports UPI, credit/debit cards, net banking, and other payment methods. This gateway integrates seamlessly with the Laravel Payments package to provide Indian users with a comprehensive payment solution.
| Method | Code | Description |
|---|---|---|
| UPI Collect | UPI_COLLECT |
UPI payment collection |
| - UPI Intent | UPI_INTENT |
UPI payment intent |
| Credit Card | CARD |
Credit and Debit Card payments |
| Net Banking | NETBANKING |
Internet banking |
| Paytm Wallet | PAYTM |
Paytm wallet payments |
| PhonePe Wallet | PHONEPE |
PhonePe wallet payments |
| Google Pay | GOOGLEPAY |
Google Pay UPI payments |
Add these variables to your .env file:
# PhonePe Configuration
PHONEPE_ENVIRONMENT=UAT
PHONEPE_MERCHANT_ID=YOUR_MERCHANT_ID
PHONEPE_SALT_KEY=YOUR_SALT_KEY
PHONEPE_SALT_INDEX=1
PHONEPE_REDIRECT_URL=https://yourapp.com/payment/phonepe/callback
PHONEPE_CALLBACK_URL=https://yourapp.com/payment/phonepe/webhook
php artisan vendor:publish --provider="Mdiqbal\LaravelPayments\PaymentsServiceProvider"
// config/payments.php
'gateways' => [
'phonepe' => [
'driver' => 'phonepe',
'environment' => env('PHONEPE_ENVIRONMENT', 'UAT'),
'merchant_id' => env('PHONEPE_MERCHANT_ID'),
'salt_key' => env('PHONEPE_SALT_KEY'),
'salt_index' => env('PHONEPE_SALT_INDEX', 1),
'redirect_url' => env('PHONEPE_REDIRECT_URL'),
'callback_url' => env('PHONEPE_CALLBACK_URL'),
'success_url' => env('PHONEPE_SUCCESS_URL'),
'failure_url' => env('PHONEPE_FAILURE_URL'),
],
],
In your PhonePe dashboard:
https://yourapp.com/payment/phonepe/webhookuse Mdiqbal\LaravelPayments\Facades\Payment;
// Initialize PhonePe gateway
$payment = Payment::gateway('phonepe');
// Create a payment request
$response = $payment->pay([
'merchantTransactionId' => 'TXN' . uniqid(),
'merchantUserId' => 'USER123',
'amount' => 10000, // Amount in paise (10000 paise = ₹100)
'redirectUrl' => route('payment.callback'),
'redirectMode' => 'REDIRECT',
'callbackUrl' => route('payment.webhook'),
'paymentInstrument' => [
'type' => 'UPI_COLLECT',
'vpa' => 'user@upi' // Optional for UPI collect
],
]);
if ($response->success) {
// Redirect to PhonePe payment page
return redirect($response->redirectUrl);
} else {
// Handle error
return back()->with('error', $response->message);
}
$response = $payment->pay([
'merchantTransactionId' => 'TXN' . uniqid(),
'merchantUserId' => 'USER123',
'amount' => 10000,
'redirectUrl' => route('payment.callback'),
'redirectMode' => 'REDIRECT',
'callbackUrl' => route('payment.webhook'),
'paymentInstrument' => [
'type' => 'CARD',
'cardNumber' => '4111111111111111', // Test card
'expiryMonth' => '12',
'expiryYear' => '25',
'cardHolderName' => 'Test User',
'cvv' => '123'
],
]);
$response = $payment->pay([
'merchantTransactionId' => 'TXN' . uniqid(),
'merchantUserId' => 'USER123',
'amount' => 10000,
'redirectUrl' => route('payment.callback'),
'redirectMode' => 'REDIRECT',
'callbackUrl' => route('payment.webhook'),
'paymentInstrument' => [
'type' => 'UPI_INTENT',
'targetApp' => 'com.phonepe.app' // or other UPI apps
],
]);
// Check payment status
$transactionId = 'TXN123456789';
$response = $payment->verify([
'merchantTransactionId' => $transactionId
]);
if ($response->success) {
echo "Payment Status: " . $response->status;
echo "Amount: " . ($response->amount / 100); // Convert paise to rupees
echo "Payment Method: " . $response->paymentInstrument->type;
}
// Process a refund
$refundResponse = $payment->refund([
'merchantTransactionId' => 'TXN123456789',
'originalTransactionId' => 'ORIG_TXN123',
'amount' => 5000, // Refund amount in paise
'refundReason' => 'Customer requested refund'
]);
if ($refundResponse->success) {
echo "Refund ID: " . $refundResponse->refundTransactionId;
}
// Check refund status
$refundStatus = $payment->verify([
'merchantTransactionId' => 'TXN123456789',
'type' => 'refund'
]);
if ($refundStatus->success) {
echo "Refund Status: " . $refundStatus->status;
echo "Refund Amount: " . ($refundStatus->amount / 100);
}
// routes/web.php
Route::post('/payment/phonepe/webhook', [PhonePeController::class, 'handleWebhook'])
->name('payment.phonepe.webhook');
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Mdiqbal\LaravelPayments\Facades\Payment;
class PhonePeController extends Controller
{
/**
* Handle PhonePe webhook
*/
public function handleWebhook(Request $request)
{
$gateway = Payment::gateway('phonepe');
// Verify webhook signature
$response = $gateway->processWebhook($request->all());
if ($response->success) {
// Extract webhook data
$webhookData = $response->data;
// Process based on event type
switch ($webhookData['type']) {
case 'PAYMENT_SUCCESS':
$this->handlePaymentSuccess($webhookData);
break;
case 'PAYMENT_FAILED':
$this->handlePaymentFailure($webhookData);
break;
case 'REFUND_STATUS':
$this->handleRefundStatus($webhookData);
break;
}
return response('Webhook processed successfully');
}
return response('Invalid webhook signature', 400);
}
/**
* Handle successful payment
*/
private function handlePaymentSuccess(array $data)
{
$paymentData = $data['data']['paymentInstrument'];
// Update your database
DB::table('payments')
->where('transaction_id', $data['data']['merchantTransactionId'])
->update([
'status' => 'completed',
'phonepe_transaction_id' => $data['data']['transactionId'],
'payment_method' => $paymentData['type'],
'amount_paid' => $data['data']['amount'] / 100,
'paid_at' => now()
]);
}
/**
* Handle failed payment
*/
private function handlePaymentFailure(array $data)
{
DB::table('payments')
->where('transaction_id', $data['data']['merchantTransactionId'])
->update([
'status' => 'failed',
'failure_reason' => $data['data']['responseCode'],
'failed_at' => now()
]);
}
/**
* Handle refund status update
*/
private function handleRefundStatus(array $data)
{
DB::table('refunds')
->where('refund_transaction_id', $data['data']['merchantTransactionId'])
->update([
'status' => $data['data']['status'],
'processed_at' => now()
]);
}
}
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Mdiqbal\LaravelPayments\Facades\Payment;
class PaymentController extends Controller
{
/**
* Handle payment callback from PhonePe
*/
public function handleCallback(Request $request)
{
$transactionId = $request->get('transactionId');
// Verify transaction status with PhonePe
$gateway = Payment::gateway('phonepe');
$response = $gateway->verify(['transactionId' => $transactionId]);
if ($response->success && $response->status === 'SUCCESS') {
// Payment successful
return redirect()->route('payment.success')
->with('transaction_id', $response->merchantTransactionId);
} else {
// Payment failed
return redirect()->route('payment.failure')
->with('error', $response->message ?? 'Payment failed');
}
}
}
PhonePe provides a UAT (User Acceptance Testing) environment for testing:
PHONEPE_ENVIRONMENT=UAT
Use these test values for UAT environment:
test@phonepe4111111111111111123// Test payment creation
$testPayment = Payment::gateway('phonepe')->pay([
'merchantTransactionId' => 'TEST_' . time(),
'merchantUserId' => 'TEST_USER',
'amount' => 100, // ₹1 in paise
'redirectUrl' => 'https://example.com/test/callback',
'callbackUrl' => 'https://example.com/test/webhook',
'paymentInstrument' => [
'type' => 'UPI_COLLECT',
'vpa' => 'test@phonepe'
],
]);
PhonePe uses X-VERIFY header authentication. The gateway automatically:
// Verify webhook is from PhonePe
$webhookHeaders = getallheaders();
$xVerify = $webhookHeaders['X-VERIFY'] ?? null;
$webhookBody = file_get_contents('php://input');
// The gateway handles verification automatically
$gateway = Payment::gateway('phonepe');
$isValid = $gateway->verifyWebhookSignature($webhookBody, $xVerify);
| Code | Description | Solution |
|---|---|---|
| BAD_REQUEST | Invalid request parameters | Check request format |
| AUTHORIZATION_FAILED | Invalid credentials | Verify merchant ID and salt key |
| INTERNAL_SERVER_ERROR | PhonePe server error | Retry after delay |
| TRANSACTION_NOT_FOUND | Invalid transaction ID | Verify transaction ID |
| TIMEOUT | Request timeout | Implement retry logic |
try {
$response = $payment->pay($paymentData);
if (!$response->success) {
// Log error details
Log::error('PhonePe payment failed', [
'code' => $response->code,
'message' => $response->message,
'transaction_id' => $paymentData['merchantTransactionId']
]);
// Show user-friendly message
return back()->with('error', 'Payment failed. Please try again.');
}
} catch (\Exception $e) {
Log::error('PhonePe gateway error', [
'error' => $e->getMessage()
]);
return back()->with('error', 'Payment service unavailable. Please try later.');
}
amount / 100// Store payment details before redirect
Session::put('phonepe_payment', [
'transaction_id' => $transactionId,
'amount' => $amount,
'user_id' => auth()->id()
]);
// Redirect to PhonePe
return redirect($paymentUrl);
// Poll for payment status if needed
$maxRetries = 10;
$retryDelay = 2; // seconds
for ($i = 0; $i < $maxRetries; $i++) {
$status = $payment->verify(['transactionId' => $transactionId]);
if ($status->status !== 'PENDING') {
break;
}
sleep($retryDelay);
}
X-VERIFY Header Error
Transaction Not Found
Webhook Not Received
Card Payment Failed
Enable debug logging to troubleshoot:
// config/payments.php
'phonepe' => [
// ... other config
'debug' => env('APP_DEBUG', false),
],
This will log all PhonePe requests and responses for debugging purposes.
How can I help you explore Laravel packages today?