Razorpay webhooks: Test Mode setup, events and signature checks
To test Razorpay webhooks, add a free webhooks.sh bin URL (no signup, 48 hours, 100 requests a day) as a Test Mode webhook and each payment event shows up live with its X-Razorpay-Signature header. Razorpay wants a 2XX within 5 seconds and retries for 24 hours before disabling the webhook.
Razorpay posts a JSON event with an X-Razorpay-Signature header to a public URL, and the quickest way to see one is a Test Mode webhook pointing at a free bin plus a test payment. This page covers setting up a Test Mode webhook, the URL rules Razorpay enforces, what the event payload looks like, how to check X-Razorpay-Signature, and the retry and auto-disable rules.
Add a free bin as a Test Mode webhook
- Create a free bin in your browser at
view.webhooks.pageor runcurl -X POST https://webhooks.sh/v1/binsto get aningest_urlandview_url. - Switch the Razorpay Dashboard to Test Mode and open
Accounts & Settings>Webhooks>+ Add New Webhook. - Paste the bin's ingest URL (such as
https://<bin>.webhooks.page/razorpay), set aSecret, selectActive Eventslikepayment.capturedandpayment.failed, and save, entering the default test OTP754081if the Dashboard asks for one. - Make a test payment.
- Watch the event arrive live in the bin viewer with its headers and JSON body.
The bin replies with HTTP 200 right away, well inside Razorpay's 5-second limit.
# Create a free bin (or open view.webhooks.page)
curl -X POST https://webhooks.sh/v1/bins
# Use the ingest_url it returns as the Webhook URL, e.g.
# https://<bin>.webhooks.page/razorpay
URL rules Razorpay enforces
- Webhook URLs must be public and use port
80or443. - A
localhostURL fails with the errorprivate ip found for host. - Domains containing
razorpayare rejected. - Razorpay blocklists several inspection and tunnel domains, including
webhook.site,requestbin.com,beeceptor.com,ngrok.ioandloca.lt(Razorpay points tozrokforlocalhost). - Endpoints must support TLS 1.2.
webhooks.page isn't on the published blocklist, and a bin's ingest URL is a public HTTPS URL served on port 443. Test Mode and Live Mode webhooks are configured separately, so a test URL never receives live payments.
What a payment event looks like
Every Razorpay webhook uses the same top-level envelope: entity (event), account_id, event (such as payment.captured), contains (such as [payment]), payload holding the entity snapshot, and created_at. Because that payload is a snapshot taken when the event happened, payment.authorized can fire after a payment is already captured and still show the authorized details. payment.captured carries only the payment entity, while order.paid carries both the order and the payment entities.
Delivery is at least once and event order isn't guaranteed, so payment.captured can arrive before payment.authorized. Use the x-razorpay-event-id header, which is unique per event, to drop duplicates.
{
"entity": "event",
"account_id": "acc_BFQ7uQEaa7j2z7",
"event": "payment.captured",
"contains": ["payment"],
"payload": {
"payment": {
"entity": {
"id": "pay_DESlfW9H8K9uqM",
"entity": "payment",
"amount": 100,
"currency": "INR",
"status": "captured",
"order_id": "order_DESlLckIVRkHWj",
"method": "netbanking",
"captured": true
}
}
},
"created_at": 1567674606
}
Verify X-Razorpay-Signature
X-Razorpay-Signature is the hex-encoded HMAC-SHA256 of the raw request body, keyed with the webhook Secret you set in the Dashboard, not your API key secret. Verify the signature against the raw body before parsing JSON, and compare digests in constant time. If you change your webhook secret, retries of older events still validate against the old secret while new events use the new one, and Razorpay's official SDKs for Node.js, Python, PHP, Ruby, Java and .NET include validation helpers.
Because the header name X-Razorpay-Signature doesn't contain authorization, cookie, key, token or secret, its value isn't redacted in the webhooks.sh bin viewer. You can copy the raw body and the signature header from the viewer into the HMAC signature verifier (set to Custom HMAC, SHA-256, hex, template {body}) to check your secret.
import { createHmac, timingSafeEqual } from "node:crypto";
export function isFromRazorpay(rawBody, signature, secret) {
const expected = createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(signature ?? "");
return a.length === b.length && timingSafeEqual(a, b);
}
Retries, auto-disable and logs
Razorpay expects a 2XX status within 5 seconds; anything else counts as a failed delivery.
When a free bin expires after 48 hours and stops accepting requests, or reaches 100 requests in a day and answers 429 (daily_cap_reached), it no longer returns a 2XX status, so Razorpay counts those deliveries as failures. After 24 hours of failures, Razorpay disables the webhook and emails the Alert Email address. Delete the test webhook or change its URL when you finish testing, and read the webhook retries guide for more on how retry schedules work.
Razorpay's limits for webhook URLs, retries, logs and replays:
| Rule | Value |
|---|---|
| Webhook URLs for payments | Up to 30 |
| RazorpayX webhooks | 1 per merchant ID |
| Allowed ports | 80 or 443 |
| Response deadline | 2XX within 5 seconds |
| Retries | Exponential back-off for 24 hours |
| After 24 hours of failures | Webhook disabled, email to the Alert Email |
| Dashboard webhook logs | Last 3 hours |
| Replay on request | Events less than 15 days old |
Keep the URL or move to localhost
If you want to keep a free bin past 48 hours, claim it from the bin page with a free Google sign-in before it expires. The 48-hour timer stops, the webhooks.page URL stays the same, and new requests are kept for 30 days on a rolling basis at 100 requests a day.
If you need a permanent endpoint or local forwarding, Solo costs $12 a month and gives you a named endpoint on webhooks.host with 250,000 requests per billing period, plus the wsh CLI (wsh listen <endpoint> --to localhost:3000) to relay live deliveries straight to your handler on localhost. See pricing for the full plan details.
FAQ
How do I test Razorpay webhooks in Test Mode?
Add a Test Mode webhook in the Razorpay Dashboard under Accounts & Settings > Webhooks using a free bin URL and a webhook secret, pick your Active Events, and make a test payment. If the Dashboard asks for an OTP when you save, enter the default test OTP 754081.
Why does Razorpay reject my webhook URL?
Razorpay rejects localhost or private IPs (which fail with private ip found for host), ports other than 80 or 443, any domain containing razorpay, and domains on its blocklist such as webhook.site, requestbin.com, beeceptor.com, ngrok.io and loca.lt.
How is X-Razorpay-Signature calculated?
X-Razorpay-Signature is the hex-encoded HMAC-SHA256 digest of the raw request body, keyed with your webhook secret rather than your API key secret.
How long does Razorpay retry a failed webhook?
Razorpay retries failed webhook deliveries with exponential back-off for 24 hours. If deliveries keep failing for the full 24 hours, Razorpay disables the webhook and sends an email to your Alert Email address.
Why did payment.captured arrive before payment.authorized?
Razorpay doesn't guarantee the order in which webhook events arrive. Check the entity status inside the payload and deduplicate deliveries using the x-razorpay-event-id header.
Where can I see Razorpay webhook logs?
In the Razorpay Dashboard under Developers > Webhooks, which shows logs from the last 3 hours only for Owner, Pseudo Owner and R&D roles. On webhooks.sh, an unclaimed free bin shows its latest 10 requests for 48 hours, or keeps 30 days of rolling history once claimed.
Last updated
Try it in 10 seconds.
Create a free bin at view.webhooks.page — no signup, no install. Point your sender at the URL and watch requests land live. Bins expire after 48 hours.