Integrating PhonePe Payment Gateway in Node.js

Chandan kumarChandan kumar
5 min read

In this blog, we will walk through the integration of PhonePe Payment Gateway in a Node.js application. This guide includes step-by-step instructions to initiate payments and handle payment status callbacks using PhonePe’s APIs.

Prerequisites

Before starting, ensure you have the following:

  1. Node.js installed on your system.

  2. A PhonePe merchant account with API credentials (merchantId and merchantSecret).

  3. A MongoDB database to store payment-related information.

Step 1: Setup Your Node.js Project

Initialize a new Node.js project and install the required dependencies:

npm init -y
npm install express mongoose body-parser axios crypto uuid

Step 2: Create Database Models

Define one Mongoose model: payments (MasterClassPayment).

const mongoose = require('mongoose');

const masterClassPaymentSchema = new mongoose.Schema({
  masterClassId: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'MasterClass',
  },
  name: {
    type: String,
  },
  email: {
    type: String,
  },
  phone: {
    type: String,
  },
  amount: {
    type: Number,
  },
  transactionId: {
    type: String,
    unique: true
  },
  merchantTransactionId: {
    type: String,
    unique: true
  },
  status: {
    type: String,
    enum: ['PENDING', 'SUCCESS', 'FAILED'],
    default: 'PENDING'
  },
  paymentResponse: {
    type: Object
  },
  createdAt: {
    type: Date,
    default: Date.now
  }
});

const MasterClassPayment = mongoose.model('MasterClassPayment', masterClassPaymentSchema);
module.exports = MasterClassPayment;

Step 3: Payment Initiation API

This API initiates a payment by sending the request to the PhonePe Payment Gateway.

const crypto = require('crypto');
const { v4: uuidv4 } = require('uuid');
const axios = require('axios');

const phonePeConfig = {
    merchantId: '', 
    merchantSecret: '', 
    paymentUrl: 'https://api.phonepe.com/v3/transaction/initiate', 
  };

Step 4. API

router.post('/master-classes/:slug/initiate-payment', async (req, res) => {
  try {
    const { slug } = req.params;
    const { name, email, phone, amount } = req.body;

    if (!amount || !slug) {
      return res.status(400).json({ 
        success: false, 
        message: 'Amount and slug are required' 
      });
    }

    const amountInPaise = Math.round(parseFloat(amount) * 100);
    console.log('Amount in paise:', amountInPaise);

    if (amountInPaise < 100) {
      return res.status(400).json({
        success: false,
        message: 'Amount must be at least 1 rupee'
      });
    }

    const masterClass = await MasterClass.findOne({ slug });
    if (!masterClass) {
      return res.status(404).json({ 
        success: false, 
        message: 'MasterClass not found' 
      });
    }

    const merchantTransactionId = generateTransactionId();
    const transactionId = `TXN_${uuidv4()}`;

    console.log('Generated merchantTransactionId:', merchantTransactionId);

    const payment = new MasterClassPayment({
      masterClassId: masterClass._id,
      name,
      email,
      phone,
      amount: amountInPaise / 100,
      transactionId,
      merchantTransactionId,
      status: 'PENDING'
    });
    await payment.save();

    const payload = {
      merchantId: phonePeConfig.merchantId,
      merchantTransactionId,
      merchantUserId: phone ? `MUID_${phone}` : `MUID_${Date.now()}`,
      name: name || '',
      amount: amountInPaise,
      redirectUrl: 'http://localhost:300/api/v1/master-class/mc/payment-status',
      redirectMode: 'POST',
      mobileNumber: phone || '',
      paymentInstrument: {
        type: 'PAY_PAGE'
      }
    };

    const payloadBase64 = Buffer.from(JSON.stringify(payload)).toString('base64');
    const keyIndex = 1;
    const string = payloadBase64 + '/pg/v1/pay' + phonePeConfig.merchantSecret;
    const sha256 = crypto.createHash('sha256').update(string).digest('hex');
    const checksum = `${sha256}###${keyIndex}`;

    const response = await axios.post(
      'https://api.phonepe.com/apis/hermes/pg/v1/pay',
      { request: payloadBase64 },
      {
        headers: {
          'Content-Type': 'application/json',
          'X-VERIFY': checksum
        }
      }
    );

    return res.status(200).json({
      success: true,
      message: 'Payment initiated',
      data: response.data,
      transactionId,
      merchantTransactionId
    });

  } catch (error) {
    console.error('Payment Initiation Error:', error);
    return res.status(500).json({
      success: false,
      message: 'Payment initiation failed',
      error: error.message
    });
  }
});


router.post('/mc/payment-status', async (req, res) => {
  try {
    const { transactionId } = req.body;

    if (!transactionId) {
      return res.status(400).json({ success: false, message: 'transactionId is required' });
    }

    // Query using PhonePe's merchantTransactionId
    const paymentRecord = await MasterClassPayment.findOne({
      merchantTransactionId: transactionId,
    });

    if (!paymentRecord) {
      return res.status(404).json({
        success: false,
        message: 'Payment record not found for the provided transactionId',
      });
    }

    // Generate checksum for PhonePe status API
    const endpointPath = `/pg/v1/status/${phonePeConfig.merchantId}/${transactionId}`;
    const stringToSign = endpointPath + phonePeConfig.merchantSecret;
    const sha256 = crypto.createHash('sha256').update(stringToSign).digest('hex');
    const checksum = `${sha256}###1`;

    // Make API request to PhonePe for status
    const phonePeResponse = await axios.get(
      `https://api.phonepe.com/apis/hermes${endpointPath}`,
      {
        headers: {
          'Content-Type': 'application/json',
          'X-VERIFY': checksum,
          'X-MERCHANT-ID': phonePeConfig.merchantId,
        },
      }
    );

    const phonePeData = phonePeResponse.data;

    // Handle Payment Success
    if (phonePeData.code === 'PAYMENT_SUCCESS') {
      const updatedPayment = await MasterClassPayment.findOneAndUpdate(
        { merchantTransactionId: transactionId },
        {
          status: 'SUCCESS',
          paymentResponse: phonePeData,
        },
        { new: true }
      );

      const masterClass = await MasterClass.findByIdAndUpdate(
        updatedPayment.masterClassId,
        {
          $push: {
            registration: {
              name: updatedPayment.name,
              email: updatedPayment.email,
              phone: updatedPayment.phone,
              payment: {
                transactionId: updatedPayment.transactionId,
                merchantTransactionId: updatedPayment.merchantTransactionId,
                status: 'SUCCESS',
                amount: updatedPayment.amount,
              },
            },
          },
        },
        { new: true }
      );

      // Render a thank-you HTML page
      return res.send(`
        <!DOCTYPE html>
        <html lang="en">
        <head>
          <meta charset="UTF-8">
          <meta name="viewport" content="width=device-width, initial-scale=1.0">
          <title>Payment Successful</title>
          <style>
            body {
              font-family: Arial, sans-serif;
              text-align: center;
              margin-top: 50px;
            }
            .container {
              max-width: 600px;
              margin: auto;
              border: 1px solid #ddd;
              padding: 20px;
              border-radius: 10px;
              box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
            }
            h1 {
              color: green;
            }
            p {
              font-size: 18px;
              margin: 10px 0;
            }
            a {
              text-decoration: none;
              color: #fff;
              background-color: #007bff;
              padding: 10px 20px;
              border-radius: 5px;
              display: inline-block;
              margin-top: 20px;
            }
            a:hover {
              background-color: #0056b3;
            }
          </style>
        </head>
        <body>
          <div class="container">
            <h1>Thank You!</h1>
            <p>Your payment of <strong>₹${(updatedPayment.amount).toFixed(2)}</strong> was successful.</p>
            <p>Transaction ID: <strong>${updatedPayment.transactionId}</strong></p>
            <p>Merchant Transaction ID: <strong>${updatedPayment.merchantTransactionId}</strong></p>
            <p>You have been successfully registered for the master class.</p>
            <a href="https://prayug.com/">Back to Home</a>
          </div>
        </body>
        </html>
      `);
    }

    // Handle Payment Failure
    await MasterClassPayment.findOneAndUpdate(
      { merchantTransactionId: transactionId },
      { status: 'FAILED', paymentResponse: phonePeData }
    );

    return res.status(200).json({
      success: false,
      message: 'Payment not successful',
      code: phonePeData.code,
    });
  } catch (error) {
    // Handle specific HTTP errors
    if (error.response) {
      return res.status(error.response.status).json({
        success: false,
        message: error.response.data.message || 'Error from PhonePe API',
        error: error.response.data,
      });
    }

    // Generic error response
    return res.status(500).json({
      success: false,
      message: 'Error while checking payment status',
      error: error.message,
    });
  }
});

Step 5: Testing the APIs

Start your Node.js server:

node server.js
  • Use a tool like Postman to test the /master-classes/:slug/initiate-payment endpoint.

  • Simulate the payment status callback using /mc/payment-status.

Conclusion

This guide helps you set up a complete integration with PhonePe Payment Gateway in your Node.js application. By following these steps, you can handle payment initiation and callbacks securely and efficiently.

0
Subscribe to my newsletter

Read articles from Chandan kumar directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Chandan kumar
Chandan kumar

Hello, World! 👋 I'm Chandan Kumar, a passionate and results-driven MEAN stack web developer with a strong background in designing and implementing web applications that deliver exceptional user experiences. I'm dedicated to staying at the forefront of web development trends and technologies to create cutting-edge solutions for my clients. My Expertise: MEAN Stack Development: I specialize in building robust web applications using MongoDB, Express.js, Angular, and Node.js. This full-stack approach allows me to create seamless, end-to-end solutions that meet and exceed client expectations. Front-end Excellence: I have a keen eye for UI/UX design and a deep understanding of front-end technologies such as HTML5, CSS3, and JavaScript. I leverage these skills to craft engaging, responsive, and user-friendly interfaces. Back-end Proficiency: With extensive experience in server-side scripting, API development, and database management, I ensure that the applications I build are not only visually appealing but also highly performant and secure. Problem Solver: I thrive on challenges and enjoy solving complex problems. Whether it's optimizing code for efficiency or troubleshooting issues, I'm committed to finding innovative solutions. My Mission: My mission is to create digital experiences that make a meaningful impact. I'm passionate about collaborating with teams and clients to turn ideas into reality. I believe that technology can empower businesses and individuals alike, and I'm excited to be a part of that journey. Let's Connect: I'm always open to networking and exploring opportunities for collaboration. Whether you're interested in discussing a potential project, sharing insights, or simply connecting with fellow professionals in the tech industry, feel free to reach out. Let's connect and start a conversation! Contact Information: 📩 Email: chandanku845415@gmail.com 📱 LinkedIn: developerchandan 🌐 Portfolio: developerchandan.github.io ✍️ Medium: developerchandan Thank you for visiting my profile, and I look forward to connecting with you!