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

Search for a command to run...

No comments yet. Be the first to comment.
Claude is incredibly good at reasoning. But reasoning is only as useful as the context available to it. Your architecture might be in GitHub. Your notes might be in Obsidian. Your decisions might be b

Mind maps are an excellent addition to an MCP ecosystem when they help users understand relationships between data, tools, and reasoning. They should complement—not replace—traditional dashboards, tab
Most AI agents today depend heavily on cloud APIs. They're fast, but every request costs money, depends on an internet connection, and sends your data to external providers. Over the weekend, I experi
How I use Fable 5 to research, build, and maintain custom Claude Code plugins, marketplaces, agents, and skills—creating reusable AI tooling that works across every project. 01 · The problem with copy

The way people search is changing. Is your site ready? For the past two decades, SEO (Search Engine Optimization) was the undisputed king of web visibility. Rank on Google's first page and you win tr
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.
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
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
// 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;
// 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 });
}
});
This is where the magic of secure verification happens.
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;
},
})
);
// 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" });
}
});
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.
Signature mismatch? Almost always caused by express.json() parsing the body before validation. Use express.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.
| 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 |
To secure even further:
Save order and user details during order creation.
On webhook, cross-check the payment details and compare with stored data before updating DB.
Use try/catch blocks and log failures for debugging real production issues.
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.