Pluto

Integrating Razorpay in a MERN Application — Full Flow with Webhooks

Integrating Razorpay in a MERN Application — Full Flow with Webhooks
Excerpt:

A complete, developer-friendly walkthrough on integrating Razorpay with the MERN stack. This guide covers creating orders, handling payments on the frontend, verifying signatures on the backend, and implementing reliable webhooks for real-world applications like crowdfunding platforms, digital stores, and SaaS products.

Payment integration is one of the most sensitive and important parts of any production-level web application. Whether it’s a donation platform, an e-commerce store, or a digital marketplace, you need something fast, secure, and reliable.

Razorpay fits perfectly in the Indian ecosystem — it’s developer-friendly, beautifully documented, and supports features like UPI, Cards, Wallets, Subscriptions, and Webhooks.

In this blog, I’ll walk you through a clean, production-ready Razorpay integration in MERN, including:

  • Creating an order on the backend
  • Initiating payment in React
  • Verifying payment signatures
  • Implementing robust webhooks
  • Handling real-world reliability issues

Screenshot 2025 12 01 at 12

1. Setting Up Razorpay on the Backend (Node.js + Express)

Install the SDK:

npm install razorpay

Create a Razorpay instance:

import Razorpay from "razorpay";

export const razorpayInstance = new Razorpay({
  key_id: process.env.RAZORPAY_KEY_ID!,
  key_secret: process.env.RAZORPAY_KEY_SECRET!,
});

Creating Orders

Your client will request an order before initiating checkout.

// POST /create-order
export const createOrder = async (req, res) => {
  try {
    const { amount } = req.body;

    const options = {
      amount: amount * 100, // in paise
      currency: "INR",
      receipt: "receipt_" + Date.now(),
    };

    const order = await razorpayInstance.orders.create(options);

    res.status(200).json({ order });
  } catch (error) {
    res.status(500).json({ message: "Order creation failed" });
  }
};

This returns an order ID that the frontend needs for checkout.

2. Implementing Razorpay Checkout on the Frontend (React)

Load the Razorpay script

const loadScript = (src) => {
  return new Promise((resolve) => {
    const script = document.createElement("script");
    script.src = src;
    script.onload = () => resolve(true);
    script.onerror = () => resolve(false);
    document.body.appendChild(script);
  });
};

Initiate payment

const startPayment = async () => {
  const res = await axios.post("/api/create-order", { amount: 500 });

  const loaded = await loadScript("https://checkout.razorpay.com/v1/checkout.js");
  if (!loaded) return alert("Failed to load payment SDK");

  const options = {
    key: import.meta.env.VITE_RAZORPAY_KEY_ID,
    amount: res.data.order.amount,
    currency: "INR",
    order_id: res.data.order.id,
    name: "My App",
    handler: async function (response) {
      await axios.post("/api/verify", response);
    },
  };

  new window.Razorpay(options).open();
};

Once payment completes, Razorpay sends back:

  • razorpay_payment_id
  • razorpay_order_id
  • razorpay_signature

pasted-image-1764573349931

3. Verifying Payment Signature (Backend)

Signature hashing ensures that the payment wasn’t forged.

import crypto from "crypto";

export const verifyPayment = async (req, res) => {
  const { razorpay_order_id, razorpay_payment_id, razorpay_signature } = req.body;

  const sign = razorpay_order_id + "|" + razorpay_payment_id;
  const expectedSign = crypto
    .createHmac("sha256", process.env.RAZORPAY_KEY_SECRET!)
    .update(sign)
    .digest("hex");

  if (razorpay_signature === expectedSign) {
    return res.status(200).json({ message: "Payment verified successfully" });
  } else {
    return res.status(400).json({ message: "Invalid signature" });
  }
};

This step is mandatory before confirming an order.

4. Implementing Webhooks (The Most Important Part)

Webhooks ensure you never lose a payment even if:

  • User closes the tab
  • Network fails
  • Frontend never receives the response
  • Server restarts during payment

Razorpay sends events like:

  • payment.captured
  • payment.failed
  • order.paid

Verify webhook signature

Razorpay signs the request using X-Razorpay-Signature.

export const handleWebhook = async (req, res) => {
  const webhookSecret = process.env.RAZORPAY_WEBHOOK_SECRET!;
  const receivedSign = req.headers["x-razorpay-signature"];

  const expectedSign = crypto
    .createHmac("sha256", webhookSecret)
    .update(JSON.stringify(req.body))
    .digest("hex");

  if (receivedSign !== expectedSign) {
    return res.status(400).send("Invalid webhook signature");
  }

  // Process event
  const event = req.body.event;

  if (event === "payment.captured") {
    // Update donation/purchase in DB
  }

  res.status(200).json({ status: "ok" });
};

Important: Use express.raw({ type: "application/json" }) for this route so Razorpay can validate the body.

pasted-image-1764573423454

5. Best Practices for Production

✔ Always rely on webhooks, not frontend confirmation

The frontend handler is nice, but webhooks are truth.

✔ Store every event in a logs table

Great for debugging and auditing.

✔ Never mark a payment as successful until webhook confirms it

This prevents fraud.

✔ Use retry logic for failed webhook processing

Razorpay retries automatically, but your server should handle duplicates gracefully.

Final Thoughts

Integrating Razorpay into a MERN application becomes smooth once you understand the order → checkout → signature verification → webhook cycle. With the webhook in place, you make the payment system robust enough for real-world apps like e-commerce, digital products, donation platforms, or subscription-based services.