Facebook Pixel

Razorpay Webhooks and Security Verifying Signatures and Preventing Fraud

Learn how to secure your Razorpay integration webhook signature verification, preventing replay attacks, idempotency, and payment security best practices.

Razorpay Webhooks and Security

Securing your payment integration is critical. This guide covers webhook security, signature verification, and fraud prevention.

1. Always Verify Webhook Signatures

router.post("/webhook", express.raw({ type: "application/json" }), asyncHandler(async (req, res) => { const webhookSecret = process.env.RAZORPAY_WEBHOOK_SECRET; const signature = req.headers["x-razorpay-signature"]; // Verify signature const expectedSignature = crypto .createHmac("sha256", webhookSecret) .update(req.body) // Must be raw body, not parsed .digest("hex"); if (expectedSignature !== signature) { console.error("Invalid webhook signature"); return res.status(400).json({ message: "Invalid signature" }); } // Signature verified process the event const event = JSON.parse(req.body.toString()); // ... handle event ... res.status(200).json({ status: "ok" }); }) );

Important: Use express.raw() for the webhook route, not express.json(). The signature is computed on the raw body.

2. Prevent Replay Attacks

A replay attack is when the same webhook is sent multiple times. Handle this with idempotency:

const handlePaymentCaptured = async (payment) => { // Check if we already processed this payment const existing = await Payment.findOne({ paymentId: payment.id }); if (existing && existing.status === "paid") { console.log("Payment already processed, skipping"); return; } // Process the payment const dbPayment = await Payment.findOne({ orderId: payment.order_id }); if (dbPayment) { dbPayment.paymentId = payment.id; dbPayment.status = "paid"; await dbPayment.save(); await User.findByIdAndUpdate(dbPayment.userId, { isPremium: true }); } };

3. Store the Webhook Secret Securely

# .env (never committed) RAZORPAY_WEBHOOK_SECRET=your_webhook_secret # Set up the webhook URL in Razorpay Dashboard: # Settings → Webhooks → Add New Webhook # URL: https://api.yourdomain.com/api/payment/webhook # Events: payment.captured, payment.failed, refund.processed # Secret: your_webhook_secret

4. Use HTTPS Only

Webhooks must be on HTTPS. Razorpay won't send webhooks to HTTP URLs in production:

# Ensure your Nginx has SSL configured # URL: https://api.yourdomain.com/api/payment/webhook

5. Never Trust Frontend Data

// WRONG trusting frontend amount router.post("/create-order", asyncHandler(async (req, res) => { const order = await razorpay.orders.create({ amount: req.body.amount * 100, // User could modify this! }); })); // RIGHT verify amount on backend router.post("/create-order", asyncHandler(async (req, res) => { const { plan } = req.body; // Get amount from your database, not the frontend const planDetails = await Plan.findOne({ name: plan }); if (!planDetails) throw new AppError("Invalid plan", 400); const order = await razorpay.orders.create({ amount: planDetails.price * 100, // Amount from database currency: "INR", receipt: `receipt_${Date.now()}` }); }));

6. Verify Payment Amount

const handlePaymentCaptured = async (payment) => { const dbPayment = await Payment.findOne({ orderId: payment.order_id }); // Verify the amount matches if (dbPayment.amount !== payment.amount) { console.error("Amount mismatch!", { expected: dbPayment.amount, received: payment.amount }); await sendAlert("Payment amount mismatch possible fraud"); return; } // Amount verified proceed dbPayment.status = "paid"; await dbPayment.save(); };

7. Log Everything

// Log all webhook events for audit await WebhookLog.create({ event: event.event, entityId: event.payload.payment?.entity?.id, signature: signature, receivedAt: new Date(), processed: true });

Security Checklist

  • Webhook signature verified on every request
  • Webhook secret in .env (not in code)
  • HTTPS only for webhook URL
  • express.raw() for webhook route (not express.json())
  • Idempotency handle duplicate webhooks
  • Amount verified against database
  • Amount comes from database, not frontend
  • API keys in .env (not in code or frontend)
  • Refunds require admin authorization
  • All webhooks logged for audit
  • Test mode used during development
  • Webhook URL not publicly documented

The Takeaway

Razorpay security involves: always verifying webhook signatures with HMAC-SHA256, using express.raw() for the webhook route (not express.json()), preventing replay attacks with idempotency checks, never trusting frontend data (get amounts from the database), verifying payment amounts match the order, using HTTPS only, storing secrets in .env, logging all webhooks for audit, and requiring admin authorization for refunds.

Use crypto.createHmac('sha256', webhookSecret).update(req.body).digest('hex') and compare with the x-razorpay-signature header. Use express.raw() for the webhook route (not express.json()), as the signature is computed on the raw body. If signatures don't match, reject the request.

Check if you've already processed the payment before handling it. Query your database for the paymentId if the status is already 'paid', skip processing and return 200. This makes webhook handling idempotent and prevents duplicate processing.

A malicious user could modify the amount in the frontend request to pay less. Always get the amount from your database based on the plan or product. The frontend sends the plan ID, the backend looks up the price, and creates the order with the database price.

Go to Razorpay Dashboard → Settings → Webhooks → Add New Webhook. Enter your HTTPS URL (https://api.yourdomain.com/api/payment/webhook), select events (payment.captured, payment.failed, refund.processed), and set a webhook secret. The secret is used to verify webhook signatures.

Log the event name, entity ID (payment ID or refund ID), signature, received timestamp, and whether it was successfully processed. Store in a WebhookLog collection for audit trail. This helps debug issues and detect suspicious activity.

Ready to master Node.js completely?

Want to upskill yourself, crack your next interview, and get your dream job? Join our comprehensive course to dive deeper with high-quality video tutorials, solve interview questions, and a premium community.

Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.