💳 Razorpay Integration with React and Node.js — Including Webhook Verification

Want to collect payments securely in your web app? Razorpay offers a clean, developer-friendly API for seamless integration. In this blog, we’ll walk through how to integrate Razorpay with a React frontend and Node.js backend — and importantly, how to securely verify payments using webhooks.
🔧 Prerequisites
Before we dive in, make sure you have:
A Razorpay account
Created your API Key (Key ID & Secret)
Setup your webhook secret from Razorpay Dashboard
A basic React frontend and Node.js Express backend
(Optional) A database like MongoDB for storing bookings or orders
🔷 Step 1: Setup Razorpay Credentials
Create a .env file on your backend:
RAZORPAY_KEY_ID=your_key_id
RAZORPAY_KEY_SECRET=your_secret_key
RAZORPAY_WEBHOOK_SECRET=your_webhook_secret
In your React project, also add:
REACT_APP_RAZORPAY_KEY_ID=your_key_id
⚛️ Step 2: Create a Payment Button in React
// components/PaymentButton.js
import React from "react";
import axios from "axios";
const PaymentButton = ({ slotData }) => {
const handlePayment = async () => {
try {
const { data } = await axios.post("/api/v1/slots/order", {
amount: slotData.price,
});
const options = {
key: process.env.REACT_APP_RAZORPAY_KEY_ID,
amount: data.amount,
currency: "INR",
order_id: data.order_id,
name: "Course Booking",
description: "Slot Booking Payment",
handler: function (response) {
alert("Payment successful! We’ll update you shortly.");
// Note: Do not trust only frontend. Use webhook.
},
prefill: {
name: slotData.name,
email: slotData.email,
contact: slotData.phone,
},
theme: {
color: "#3399cc",
},
};
const razorpay = new window.Razorpay(options);
razorpay.open();
} catch (err) {
console.error("Error initiating payment", err);
}
};
return <button onClick={handlePayment}>Pay Now</button>;
};
export default PaymentButton;
🧠 Step 3: Create Razorpay Order in Node.js Backend
// routes/slots.js
import express from "express";
import Razorpay from "razorpay";
const router = express.Router();
const razorpay = new Razorpay({
key_id: process.env.RAZORPAY_KEY_ID,
key_secret: process.env.RAZORPAY_KEY_SECRET,
});
router.post("/slots/order", createOrder, async (req, res) => {
try {
const order = await Razorpay.orders.create({
amount: req.validatedBody.amount * 100,
currency: "INR",
receipt: `receipt_${Date.now()}`,
});
const newSlot = new Slot({
order_id: order.id,
price: req.validatedBody.amount,
payment_status: "pending",
createdAt: new Date(),
updatedAt: new Date(),
});
await newSlot.save();
return res.status(200).json({
order_id: order.id,
currency: order.currency,
amount: order.amount,
});
} catch (err) {
console.error("Error during payment:", err);
return res.status(500).json({ status: "failed", error: err.message });
}
});
📡 Step 4: Setup Razorpay Webhook on Backend
This is where the magic of secure verification happens.
🧩 Update your middleware in app.js or server.js:
import express from "express";
const app = express();
app.use("/slots/webhook", express.raw({ type: "application/json" })); // before express.json()
app.use(express.json());
app.use(
bodyParser.json({
verify: (req, res, buf) => {
req.rawBody = buf;
},
})
);
🔐 Handle Webhook Route
// routes/slots.js
import crypto from "crypto";
async function validateWebhookSignature(payload, signature, secret) {
const generatedSignature = crypto
.createHmac("sha256", secret)
.update(payload, "utf8")
.digest("hex");
return generatedSignature === signature;
}
router.post("/slots/webhook", async (req, res) => {
try {
const webhookSecret = process.env.RAZORPAY_WEBHOOK_SECRET;
if (!webhookSecret) {
return res.status(500).json({ message: "Webhook secret missing" });
}
const receivedSignature = req.headers["x-razorpay-signature"];
let isValid = await validateWebhookSignature(
JSON.stringify(JSON.parse(req.rawBody)),
receivedSignature,
webhookSecret
);
if (!isValid) {
return res.status(400).json({ message: "Invalid Signature" });
}
const event = req.body.event;
if (event === "payment.captured") {
const { order_id, id, amount, status, method, email } =
req.body.payload.payment.entity;
await Slot.findOneAndUpdate(
{ order_id },
{
payment_status: "captured",
payment_method: method,
email: email,
payment_id: id,
updatedAt: new Date(),
isBooked: true,
}
);
res
.status(200)
.json({ message: "Payment Captured & Updated Successfully" });
} else {
console.warn(`Unhandled Webhook Event: ${event}`);
res.status(400).json({ message: "Unhandled Webhook Event" });
}
} catch (err) {
console.error("Error in webhook", err);
res.status(500).json({ message: "Internal Server Error" });
}
});
🧪 Step 5: Test Razorpay Webhooks Locally
Since Razorpay can't reach localhost, use ngrok to expose your local server:
npx ngrok http 5000
Then set https://your-ngrok-url/slots/webhook as your webhook URL in Razorpay Dashboard and select payment.captured as the event.
🧠 What I Learned from Debugging
Signature mismatch? Almost always caused by
express.json()parsing the body before validation. Useexpress.raw()for webhook route.Never rely only on Razorpay's frontend
handler— users could manipulate it. Webhooks are your source of truth.You can also log webhook payloads during testing and compare them with your expected values.
✅ Summary
| Part | Tech / Tool |
| UI Button | React + Razorpay JS SDK |
| Order Creation | Node.js (Express) |
| Payment Modal | Razorpay Checkout |
| Verification | Razorpay Webhook |
| DB Update | MongoDB / Mongoose |
🚀 Bonus Tip
To secure even further:
Save order and user details during
ordercreation.On webhook, cross-check the payment details and compare with stored data before updating DB.
Use
try/catchblocks and log failures for debugging real production issues.
🔚 Final Thoughts
Building your own payment flow is powerful and surprisingly straightforward. Razorpay’s developer tools make it smooth, but real-time verification is the key to trust and reliability in your system. With this setup, your app can handle payments and bookings securely, scalably, and professionally.




