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

  1. Create a free bin in your browser at view.webhooks.page or run curl -X POST https://webhooks.sh/v1/bins to get an ingest_url and view_url.
  2. Switch the Razorpay Dashboard to Test Mode and open Accounts & Settings > Webhooks > + Add New Webhook.
  3. Paste the bin's ingest URL (such as https://<bin>.webhooks.page/razorpay), set a Secret, select Active Events like payment.captured and payment.failed, and save, entering the default test OTP 754081 if the Dashboard asks for one.
  4. Make a test payment.
  5. 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 80 or 443.
  • A localhost URL fails with the error private ip found for host.
  • Domains containing razorpay are rejected.
  • Razorpay blocklists several inspection and tunnel domains, including webhook.site, requestbin.com, beeceptor.com, ngrok.io and loca.lt (Razorpay points to zrok for localhost).
  • 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:

RuleValue
Webhook URLs for paymentsUp to 30
RazorpayX webhooks1 per merchant ID
Allowed ports80 or 443
Response deadline2XX within 5 seconds
RetriesExponential back-off for 24 hours
After 24 hours of failuresWebhook disabled, email to the Alert Email
Dashboard webhook logsLast 3 hours
Replay on requestEvents 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.