Introduction
PhonePe payment gateway integration provides a seamless way to accept payments in your web applications. This comprehensive guide will walk you through the step-by-step process of integrating PhonePe payment gateway into your website.
Note: Make sure you have a PhonePe merchant account before proceeding with
the integration.
Implementation Steps
Structure
βββ πpay
βββ πutils
βββ common.php
βββ config.php
βββ SendMail.php
βββ πvendor
βββ composer.json
βββ composer.lock
βββ dbcon.php
βββ failure.php
βββ index.php
βββ log.txt
βββ notes.txt
βββ pay.php
βββ paymentstatus.php
βββ success.php
config.php
<?php
// config.php
define("BASE_URL", "http://localhost/pay/");
define("API_STATUS", "UAT");
define("MERCHANTIDLIVE", "your live key");
// define("MERCHANTIDUAT", "your merchant if");
// define("SALTKEYUAT", "your merchant salt key");
define("SALTKEYLIVE", "you key");
define("SALTINDEX", "1");
define("REDIRECTURL", "paymentstatus.php");
define("SUCCESSURL", "success.php");
define("FAILUREURL", "failure.php");
define("SALTKEYUAT", "your salt key");
define("MERCHANTIDUAT", "your merchant id");
// Payment API URLs
define("UATURLPAY", "https://api-preprod.phonepe.com/apis/hermes/pg/v1/pay");
define("LIVEURLPAY", "https://api.phonepe.com/apis/hermes/pg/v1/pay");
// Status Check URLs
define("STATUSCHECKURL", "https://api-preprod.phonepe.com/apis/hermes/pg/v1/status/");
define("LIVESTATUSCHECKURL", "https://api.phonepe.com/apis/hermes/pg/v1/status/");
?>
Payment Form (index.php)
<form id="form" action="pay.php" method="POST">
<input id="name" type="text" name="name" placeholder="Full Name*" required>
<input id="email" type="email" name="email" placeholder="Email*" required>
<input id="phone" type="tel" name="contact" placeholder="Contact Number*" required>
<input id="amount" type="number" name="amount" placeholder="Amount*" required step="0.01">
<input type="submit" value="Proceed to Payment" name="submit">
</form>
pay.php
<?php
session_start();
require_once "./utils/config.php";
require_once "./utils/common.php";
include "./dbcon.php";
// Initialize error handler
function handleError($message) {
error_log($message);
die($message);
}
// Validate POST data
if (!isset($_POST['name'], $_POST['email'], $_POST['contact'], $_POST['amount'])) {
handleError("Error: Missing required POST data.");
}
// Clean and validate input data
$name = trim(strip_tags($_POST['name']));
$email = filter_var(trim($_POST['email']), FILTER_VALIDATE_EMAIL);
$contact = preg_replace('/[^0-9]/', '', $_POST['contact']);
$amount = (float) $_POST['amount'];
// Validate each field
if (empty($name)) {
handleError("Error: Name is required.");
}
if (!$email) {
handleError("Error: Invalid email format.");
}
if (strlen($contact) < 10) {
handleError("Error: Invalid contact number.");
}
if ($amount <= 0) {
handleError("Error: Invalid amount.");
}
// Generate unique transaction ID
$transactionId = "MT-" . uniqid() . "-" . time();
$merchantId = (API_STATUS === "LIVE") ? MERCHANTIDLIVE : MERCHANTIDUAT;
// Store in session
$_SESSION['payment_data'] = [
'name' => $name,
'email' => $email,
'contact' => $contact,
'amount' => $amount,
'transaction_id' => $transactionId,
'merchant_id' => $merchantId,
'timestamp' => time()
];
// Verify session storage
if (!isset($_SESSION['payment_data'])) {
handleError("Error: Failed to store session data.");
}
// Log payment initiation
error_log("Payment Initiation - Transaction: {$transactionId}, Amount: {$amount}, Email: {$email}");
// Store initial payment record
try {
$stmt = $con->prepare("
INSERT INTO payment_attempts
(transaction_id, merchant_id, name, email, contact, amount, status, created_at)
VALUES (?, ?, ?, ?, ?, ?, 'INITIATED', NOW())
");
$stmt->bind_param("sssssd",
$transactionId,
$merchantId,
$name,
$email,
$contact,
$amount
);
if (!$stmt->execute()) {
throw new Exception("Failed to store payment record: " . $stmt->error);
}
$stmt->close();
} catch (Exception $e) {
error_log($e->getMessage());
// Continue even if DB insert fails - don't block payment
}
// Prepare payment gateway request
$saltKey = (API_STATUS === "LIVE") ? SALTKEYLIVE : SALTKEYUAT;
$url = (API_STATUS === "LIVE") ? LIVEURLPAY : UATURLPAY;
$saltIndex = SALTINDEX;
$payLoad = [
'merchantId' => $merchantId,
'merchantTransactionId' => $transactionId,
'merchantUserId' => "MUID-" . uniqid(),
'amount' => $amount * 100,
'redirectUrl' => BASE_URL . REDIRECTURL,
'redirectMode' => "POST",
'callbackUrl' => BASE_URL . REDIRECTURL,
'mobileNumber' => $contact,
'paymentInstrument' => [
'type' => "PAY_PAGE",
],
];
$jsonEncodedPayload = json_encode($payLoad);
$payloadBase64 = base64_encode($jsonEncodedPayload);
$dataToHash = $payloadBase64 . "/pg/v1/pay" . $saltKey;
$checksum = hash("sha256", $dataToHash) . "###" . $saltIndex;
$requestPayload = json_encode(['request' => $payloadBase64]);
$headers = [
"Content-Type: application/json",
"X-VERIFY: " . $checksum,
"accept: application/json",
];
// Payment gateway request with retry mechanism
$retryCount = 0;
$maxRetries = 3; // Reduced from 5 to 3 to minimize testing costs
while ($retryCount < $maxRetries) {
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_SSL_VERIFYHOST => 0,
CURLOPT_SSL_VERIFYPEER => 0,
CURLOPT_TIMEOUT => 30,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => $requestPayload,
CURLOPT_HTTPHEADER => $headers,
]);
$response = curl_exec($curl);
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
$error = curl_error($curl);
curl_close($curl);
if ($error) {
error_log("Payment cURL Error for {$transactionId}: " . $error);
$retryCount++;
continue;
}
$responseDecoded = json_decode($response, true);
if (isset($responseDecoded['success']) && $responseDecoded['success'] === true) {
// Update payment record status
try {
$stmt = $con->prepare("
UPDATE payment_attempts
SET status = 'REDIRECTED',
updated_at = NOW()
WHERE transaction_id = ?
");
$stmt->bind_param("s", $transactionId);
$stmt->execute();
$stmt->close();
} catch (Exception $e) {
error_log("Failed to update payment status: " . $e->getMessage());
// Continue even if update fails
}
// Redirect to payment gateway
$paymentUrl = $responseDecoded['data']['instrumentResponse']['redirectInfo']['url'];
header("Location: " . $paymentUrl);
exit;
} elseif (isset($responseDecoded['code']) && $responseDecoded['code'] === "TOO_MANY_REQUESTS") {
$waitTime = pow(2, $retryCount);
error_log("Payment retry {$retryCount} for {$transactionId}");
sleep($waitTime);
$retryCount++;
} else {
error_log("Payment error for {$transactionId}: " . json_encode($responseDecoded));
$retryCount++;
}
}
// If we reach here, all retries failed
handleError("Payment initiation failed after {$maxRetries} attempts. Please try again later.");
?>
paymentstatus.php
<?php
session_start();
require_once "./utils/config.php";
require_once "./utils/common.php";
require_once "./utils/SendMail.php";
// Validate POST data from payment gateway
$transactionId = $_POST['transactionId'] ?? null;
$merchantId = $_POST['merchantId'] ?? null;
if (!$transactionId || !$merchantId) {
error_log("Missing transaction or merchant ID in callback");
header("Location: " . BASE_URL . "error.php?message=invalid_response");
exit;
}
// Verify session data exists and matches
if (!isset($_SESSION['payment_data']) ||
$_SESSION['payment_data']['transactionId'] !== $transactionId ||
$_SESSION['payment_data']['merchantId'] !== $merchantId) {
error_log("Session validation failed for transaction: {$transactionId}");
// Attempt to recover from database
require_once "./dbcon.php";
$stmt = $con->prepare("SELECT * FROM payment_attempts WHERE transaction_id = ? AND merchant_id = ? ORDER BY created_at DESC LIMIT 1");
$stmt->bind_param("ss", $transactionId, $merchantId);
$stmt->execute();
$result = $stmt->get_result();
$paymentData = $result->fetch_assoc();
$stmt->close();
if ($paymentData) {
// Reconstruct session data from database
$_SESSION['payment_data'] = [
'email' => $paymentData['email'],
'amount' => $paymentData['amount'],
'transactionId' => $paymentData['transaction_id'],
'merchantId' => $paymentData['merchant_id'],
'name' => $paymentData['name'],
'contact' => $paymentData['contact']
];
} else {
error_log("Could not recover payment data from database for transaction: {$transactionId}");
header("Location: " . BASE_URL . "error.php?message=session_expired");
exit;
}
}
// Get data from session
$amount = $_SESSION['payment_data']['amount'];
$email = $_SESSION['payment_data']['email'];
$name = $_SESSION['payment_data']['name'];
$contact = $_SESSION['payment_data']['contact'];
// Check if payment attempt already exists
require_once "./dbcon.php";
$stmt = $con->prepare("SELECT id FROM payment_attempts WHERE transaction_id = ? AND merchant_id = ?");
$stmt->bind_param("ss", $transactionId, $merchantId);
$stmt->execute();
$result = $stmt->get_result();
$existingPayment = $result->fetch_assoc();
$stmt->close();
if (!$existingPayment) {
// Insert new payment attempt
$stmt = $con->prepare("
INSERT INTO payment_attempts (
transaction_id, merchant_id, email, amount, status,
name, contact, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
");
$initialStatus = "PAYMENT_INITIATED";
$stmt->bind_param("sssssss",
$transactionId, $merchantId, $email, $amount, $initialStatus,
$name, $contact
);
$stmt->execute();
$stmt->close();
}
// API configuration
$url = (API_STATUS === "LIVE")
? LIVESTATUSCHECKURL . $merchantId . "/" . $transactionId
: STATUSCHECKURL . $merchantId . "/" . $transactionId;
$saltKey = (API_STATUS === "LIVE") ? SALTKEYLIVE : SALTKEYUAT;
$saltIndex = SALTINDEX;
// Prepare checksum
$dataToHash = "/pg/v1/status/" . $merchantId . "/" . $transactionId . $saltKey;
$checksum = hash("sha256", $dataToHash) . "###" . $saltIndex;
// Send GET request
$headers = [
"Content-Type: application/json",
"accept: application/json",
"X-VERIFY: " . $checksum,
"X-MERCHANT-ID: " . $merchantId,
];
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
$response = curl_exec($curl);
$error = curl_error($curl);
curl_close($curl);
if ($error) {
error_log("Status check cURL error for transaction {$transactionId}: {$error}");
header("Location: " . BASE_URL . "error.php?message=status_check_failed");
exit;
}
$responseDecoded = json_decode($response, true);
// Update final payment status
$finalStatus = isset($responseDecoded['code']) ? $responseDecoded['code'] : 'UNKNOWN';
$stmt = $con->prepare("
UPDATE payment_attempts
SET status = ?,
updated_at = NOW(),
response_data = ?
WHERE transaction_id = ?
AND merchant_id = ?
");
$responseJson = json_encode($responseDecoded);
$stmt->bind_param("ssss", $finalStatus, $responseJson, $transactionId, $merchantId);
$stmt->execute();
$stmt->close();
if (isset($responseDecoded['success']) && $responseDecoded['code'] === "PAYMENT_SUCCESS") {
try {
$_SESSION['payment_success'] = [
'transactionId' => $transactionId,
'email' => $email,
'amount' => $amount,
'name' => $name,
'contact' => $contact
];
// $mailer = new Mail();
// $mailer->sendPaymentConfirmation($email, $name, $transactionId, $amount);
header("Location: " . BASE_URL . "success.php?tid=" . $transactionId);
exit;
} catch (Exception $e) {
error_log("Mailer error: " . $e->getMessage());
return false;
}
} else {
// Log the failure
error_log("Payment failed for transaction {$transactionId}: " . json_encode($responseDecoded));
// Clear session after failed payment
unset($_SESSION['payment_data']);
// Redirect to failure page
header("Location: " . BASE_URL . "failure.php?tid=" . $transactionId);
exit;
}
?>
success.php
<?php
session_start();
if (isset($_SESSION['payment_success'])) {
$paymentData = $_SESSION['payment_success'];
unset($_SESSION['payment_success']);
} else {
header("Location: " . BASE_URL . "error.php?message=session_expired");
exit;
}
?>
<div class="success-card">
<div class="checkmark-circle">
<span class="checkmark">β</span>
</div>
<h1>Payment Successful!</h1>
<div class="transaction-details">
<div class="detail-item">
<span class="detail-label">Transaction ID</span>
<span class="detail-value"><?php echo htmlspecialchars($paymentData['transactionId']); ?></span>
</div>
<div class="detail-item">
<span class="detail-label">Email</span>
<span class="detail-value"><?php echo htmlspecialchars($paymentData['email']); ?></span>
</div>
<div class="detail-item">
<span class="detail-label">Amount</span>
<span class="detail-value"><?php echo htmlspecialchars($paymentData['amount']); ?></span>
</div>
</div>
<p class="success-message">
Thank you for your payment! We've received your purchase request<br>and will process it shortly.
</p>
</div>
failure.php
<?php print_r($_GET); ?>
SendMail.php
<?php
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
require './vendor/autoload.php';
class Mail {
private $mailer;
public function __construct() {
$this->mailer = new PHPMailer(true);
$this->mailer->isSMTP();
$this->mailer->Host = 'smtp.hostinger.com';
$this->mailer->SMTPAuth = true;
$this->mailer->Username = 'your username';
$this->mailer->Password = 'your password';
$this->mailer->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
$this->mailer->Port = 465;
$this->mailer->setFrom('your email', 'Payment Status');
$this->mailer->isHTML(true);
}
public function sendPaymentConfirmation($to, $name, $transactionId, $amount) {
try {
// Set recipient and subject
$this->mailer->addAddress($to);
$this->mailer->Subject = 'Payment Confirmation';
// Email body content (HTML version)
$this->mailer->Body = "
<h1>Payment Confirmation</h1>
<p>Dear {$name},</p>
<p>Thank you for your payment. Your transaction has been successfully processed.</p>
<p><strong>Transaction ID:</strong> {$transactionId}</p>
<p><strong>Amount Paid:</strong> {$amount}</p>
<p>Best regards,<br>Your Name</p>
";
// Send the email
if ($this->mailer->send()) {
return "Email sent successfully!";
} else {
return "Failed to send email.";
}
} catch (Exception $e) {
return "Mailer Error: " . $this->mailer->ErrorInfo;
}
}
}
?>
payment_attempts (table)
CREATE TABLE `payment_attempts` (
`id` int(11) NOT NULL,
`transaction_id` varchar(100) NOT NULL,
`merchant_id` varchar(100) NOT NULL,
`name` varchar(255) NOT NULL,
`email` varchar(255) NOT NULL,
`contact` varchar(20) NOT NULL,
`amount` decimal(10,2) NOT NULL,
`status` varchar(50) NOT NULL,
`response_data` text DEFAULT NULL,
`error_message` text DEFAULT NULL,
`created_at` datetime NOT NULL,
`updated_at` datetime DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
Configuration Settings
Before implementing the payment gateway, make sure to configure these essential settings in your PhonePe merchant dashboard:
- API Keys and Credentials
- Webhook URL Configuration
- Payment Success/Failure URLs
- Transaction Limits