CrazyDev AI Assistant
πŸ€–

CrazyDev Assistant

Online
Welcome to CrazyDev!
I'm your AI assistant, ready to help with web development services, project inquiries, and technical questions.
Just a sec…
C R A Z Y D E V

PhonePe Payment Gateway Integration

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

Testing

Use these test credentials for development:

Merchant ID: TEST_MERCHANT_123
API Key: TEST_API_KEY_456

Debit Card:
Card Number: 4242 4242 4242 4242
Card Type: DEBIT_CARD
Card Issuer: VISA
Expiry Month: 12
Expiry Year: 2023
CVV: 936

Credit Card:
Card Number: 4208 5851 9011 6667
Card Type: CREDIT_CARD
Card Issuer: VISA
Expiry Month: 06
Expiry Year: 2027
CVV: 508

Note: The OTP to be used on the Bank Page: 123456