Guide to Razorpay Webhooks: Features and Best Practices
Razorpay webhooks notify your application about payment activity: a payment is captured or fails, an order is paid, a refund is processed, a subscription is charged. If you're building payments on Razorpay, webhooks are how you react to these events reliably without polling.
This guide covers how Razorpay webhooks work, the events you'll handle, how to verify the X-Razorpay-Signature, and the best practices for production.
What are Razorpay webhooks?
Razorpay webhooks are JSON POSTs delivered to a URL you configure in the dashboard. Each is signed with an X-Razorpay-Signature header: an HMAC-SHA256 (hex) over the raw request body, keyed with the webhook secret you set in the dashboard. That secret is a value you choose, and it's separate from your API Key ID and secret. The official Node SDK exposes Razorpay.validateWebhookSignature for this.
Razorpay webhook features
| Feature | Details |
|---|---|
| Configuration | Dashboard > Settings > Webhooks (separate webhooks for Live and Test) |
| Signature header | X-Razorpay-Signature |
| Signature scheme | HMAC-SHA256 (hex) over the raw body, keyed with the dashboard webhook secret |
| Event location | The event field in the JSON body (not a header) |
| Amounts | In the smallest currency unit (paise for INR: 5000 = ₹50.00) |
| SDK | razorpay Razorpay.validateWebhookSignature (Node); manual HMAC for Python |
Common events
Razorpay reports the event type in the body's top-level event field:
| Event | Fires when |
|---|---|
payment.authorized | A payment is authorized (not yet captured) |
payment.captured | A payment is captured |
payment.failed | A payment attempt fails |
order.paid | An order is fully paid |
refund.created / refund.processed / refund.failed | A refund changes state |
subscription.charged / subscription.activated / subscription.cancelled | A subscription changes |
The payload nests entity data by the keys in contains (for example payload.payment.entity).
Setting up Razorpay webhooks
In the dashboard, go to Settings > Webhooks > Add New Webhook, enter an HTTPS URL, and choose the active events. The webhook secret is a value you set (not generated), store it as RAZORPAY_WEBHOOK_SECRET, separate from your API key. Live mode and Test mode have separate webhooks and separate secrets, so make sure you're using the right one.
Securing Razorpay webhooks
Use the official SDK's validateWebhookSignature(body, signature, secret) in Node, passing the raw body. In Python (no SDK helper exists), compute an HMAC-SHA256 hex over the raw body and compare in constant time. Verify against the raw body before parsing.
const Razorpay = require("razorpay");
const SECRET = process.env.RAZORPAY_WEBHOOK_SECRET;
function verify(rawBody, signature) {
if (!signature) return false;
try {
return Razorpay.validateWebhookSignature(rawBody.toString(), signature, SECRET);
} catch {
return false;
}
}
app.post("/webhooks/razorpay", express.raw({ type: "application/json" }), (req, res) => {
if (!verify(req.body, req.headers["x-razorpay-signature"])) {
return res.sendStatus(400);
}
res.sendStatus(200); // acknowledge fast
const payload = JSON.parse(req.body.toString());
processQueue.add(payload); // branch on payload.event, async
});
The same check in Python (manual, constant-time):
import hashlib
import hmac
import os
SECRET = os.environ["RAZORPAY_WEBHOOK_SECRET"].encode()
def verify(raw_body: bytes, signature_header: str) -> bool:
if not signature_header:
return False
expected = hmac.new(SECRET, raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature_header)
Razorpay webhook limitations and pain points
The secret is the webhook secret, not the API key
The Problem: Signatures are keyed with the dashboard webhook secret you chose, not your API Key secret. Using the API key never matches.
Why It Happens: Razorpay separates the webhook secret from API credentials.
Workarounds:
- Key the HMAC with
RAZORPAY_WEBHOOK_SECRET, the value you set on the webhook.
How Hookdeck Can Help: Hookdeck verifies the signature at the edge, so the correct secret lives in one place.
Live and Test have separate secrets
The Problem: Live mode and Test mode use different webhooks and different secrets. A wrong-mode secret fails every request.
Why It Happens: Razorpay isolates Live and Test.
Workarounds:
- Configure and store both secrets, and select the one matching the mode.
How Hookdeck Can Help: Hookdeck can front both environments and verify each with its own secret.
The SDK compares without constant time
The Problem: validateWebhookSignature uses plain string equality internally. When you self-implement, a non-constant-time compare leaks timing information.
Why It Happens: The SDK helper prioritizes simplicity.
Workarounds:
- Prefer a timing-safe comparison in any manual implementation.
How Hookdeck Can Help: Hookdeck verifies with a constant-time comparison at the edge.
The event type is in the body
The Problem: The event type is a body field (event), not a header, and amounts are in paise. Code that reads a header, or treats amounts as rupees, gets it wrong.
Why It Happens: Razorpay puts the event in the payload and reports amounts in the smallest unit.
Workarounds:
- Branch on
payload.eventafter verifying, and convert amounts from paise.
How Hookdeck Can Help: Hookdeck's filters can route on the event field you receive.
Best practices
Verify HMAC-SHA256 over the raw body with the webhook secret
Use validateWebhookSignature in Node, or a constant-time hex HMAC in Python, over the raw body.
Use the right secret for the mode
Store separate Live and Test secrets and match them to the environment.
Return 2xx and make handlers idempotent
Acknowledge with 2xx or Razorpay retries; branch on event and dedupe.
Process asynchronously
Defer work to a queue after acknowledging. See why to process webhooks asynchronously.
Make Razorpay webhooks production-ready
Hookdeck verifies X-Razorpay-Signature, deduplicates, and durably queues every payment event
Conclusion
Razorpay webhooks are verified with an X-Razorpay-Signature HMAC-SHA256 over the raw body, hex-encoded and keyed with the dashboard webhook secret (distinct from your API key). Use the official Razorpay.validateWebhookSignature in Node or a constant-time HMAC in Python, use the right secret for Live vs Test, branch on the body event, and make handlers idempotent.
Hookdeck verifies the signature, deduplicates, and durably queues every event at the edge, so your app only ever processes verified, unique events.
Get started with Hookdeck for free and handle Razorpay webhooks reliably in minutes.