Skip to content
TryWebhook
New URL

Payments · Free · No signup

Test and debug Razorpay webhooks

Point a Razorpay webhook at a throwaway URL, capture a payment event, and read the nested envelope exactly as it arrived — plus the hex HMAC you can check against your webhook secret without leaving the browser.

No signup · No email · Ready in about a second

At a glance

Razorpay webhooks in brief

Category
Payments
Where to configure
Razorpay Dashboard → Account & Settings → Webhooks → Add New Webhook
Signature header
X-Razorpay-Signature
Secret
Webhook secret (the one you typed when creating the webhook)
Retries
Retried with increasing delays for up to 24 hours. Deliveries and their responses are visible in the Dashboard against each webhook.
Response deadline
5 seconds. Acknowledge and queue; do not settle orders inline.
Treated as success
Any 2xx. Everything else is retried.

Cross-checked against Razorpay's own webhook documentation. Providers change these; if something here is stale, tell us.

The payload

What Razorpay actually sends

Shape-accurate, with obviously fake identifiers. These are the same samples the inspector's Send test webhook panel fires, so you can read one here and then send the identical bytes to your own URL.

POST/w/{your-bin-id}418 B

Note the paise amounts and the nested `entity` envelope.

1 headers
user-agent
Razorpay-Webhook/1.0
Body · application/json · 1 line
{"entity":"event","account_id":"acc_FakeAccount","event":"payment.captured","contains":["payment"],"created_at":1750000000,"payload":{"payment":{"entity":{"id":"pay_FakePaymentId","entity":"payment","amount":249900,"currency":"INR","status":"captured","order_id":"order_FakeOrderId","method":"upi","captured":true,"email":"buyer@example.com","contact":"+919999999999","fee":5898,"tax":899,"notes":{"plan":"annual"}}}}}

No sample carries a signature header. A valid one can only be produced with your own signing secret, and a fabricated one would make the Signature tab report a mismatch that isn't real. Send a genuine delivery when you want to test verification.

Verification

Verifying a Razorpay signature

Signed payload

A hex-encoded HMAC-SHA256 of the raw request body, with no prefix and no timestamp, keyed with the webhook secret you chose when you created the webhook.

Implemented as: HMAC-SHA256 over the raw body, hex.

Built in. Open any captured request, switch to the Signature tab, pick Razorpay, and paste your webhook secret (the one you typed when creating the webhook). The HMAC is computed in your browser with the Web Crypto API over the raw bytes that arrived — the secret is never sent to us, and never written to disk.

Events

Events worth subscribing to first

Not the full catalogue — the handful that carry most integrations. Subscribing to everything is the fastest way to a handler that times out.

EventFires when
payment.capturedFunds were captured. The event to provision on for one-off payments — but note that `order.paid` may also fire for the same transaction.
payment.failedAn attempt failed. Carries `error_code`, `error_description`, and `error_reason`, which are far more specific than what the checkout returns to the browser.
order.paidEvery payment against an order has succeeded. Fires alongside `payment.captured` for single-payment orders, which is where double-provisioning comes from.
subscription.chargedA subscription cycle was billed. The payload contains both the subscription and the payment entity.
refund.processedA refund reached the customer's bank. `refund.created` fires earlier and does not mean the money has moved.

Setting up a webhook against a test URL

Create a URL above, then in the Razorpay Dashboard go to Account & Settings → Webhooks → Add New Webhook. Paste the URL, invent a secret and note it down, and select the events. Stay in Test mode while you are doing this.

Razorpay sends nothing at creation. To make an event happen, run a test payment through Checkout with one of the test card numbers, or create and pay a test order through the API.

The envelope, layer by layer

Razorpay’s shape is deeper than most. Reading it from the outside in:

{
  "entity": "event",
  "event": "payment.captured",
  "contains": ["payment"],
  "payload": {
    "payment": {
      "entity": { "id": "pay_...", "amount": 249900, ... }
    }
  }
}

Four things to take from that:

  • event is the type. It is in the body, not a header — the opposite of GitHub and Shopify.
  • contains lists which keys exist under payload. Read it rather than guessing; a subscription charge contains both payment and subscription.
  • The actual resource is one level further down, at payload.<name>.entity.
  • amount is paise.

So a handler looks like this:

const { event, payload } = body;

if (event === 'payment.captured') {
  const payment = payload.payment.entity;
  await markPaid(payment.order_id, payment.amount / 100);
}

Notes are your join key

"notes": { "plan": "annual" }

notes is the field to plan around. It is the only place you can put your own identifiers when creating the order, and it comes back on every event about that payment. Without it you are matching on email addresses, which breaks the first time a customer pays from a different one.

Verifying X-Razorpay-Signature

X-Razorpay-Signature: 3f1d8a...

Hex HMAC-SHA256 over the raw body, keyed with the webhook secret. No timestamp, no prefix, nothing else in the signed string. In the Signature tab above, pick Razorpay and paste the secret.

The scheme is identical in construction to Lemon Squeezy’s and differs from GitHub’s only in the missing sha256= prefix. If you have a verifier for one, the port is trivial — which is exactly why people forget to change the secret and spend an afternoon on it.

Because the signature covers only the body, deliveries are replayable forever. Deduplicate on the event id in the payload.

One webhook URL receives everything you subscribed to across the whole account: payments, orders, refunds, subscriptions, payment links, settlements. There is no per-product routing. Your handler needs a switch with an explicit default that returns 200, or the first settlement.processed you did not plan for will start a 24-hour retry loop.

Debugging a failing endpoint

The Dashboard shows the response code Razorpay received against each delivery. Match what you see there to what arrived here:

  • Nothing here, failures there — the URL or the mode is wrong. Test-mode webhooks and live-mode webhooks are separate lists.
  • Arrived here, 401 there — wrong secret, or the body was parsed before verification.
  • Arrived here, timeout there — the five-second budget. Queue the event and return immediately.

Copy the request as cURL to replay it against your own endpoint. The signature header comes along with it, so a verifier that rejects the replay would have rejected Razorpay.

Failure modes

Where Razorpay integrations usually break

Amounts are in paise

`amount: 249900` is ₹2,499.00, not ₹249,900. Every amount in the payload — `amount`, `fee`, `tax`, `amount_refunded` — is an integer of the smallest currency unit. Dividing by 100 at the boundary and never again is the only way to keep this straight.

The webhook secret is not your API secret

Razorpay has two different secrets: the API key secret used for REST calls, and a per-webhook secret you invent when creating the webhook. Signature verification uses the second one. Using the first is the most common cause of a permanent 401 loop.

Two levels of envelope

The entity you want is at `payload.payment.entity`, not `payload.payment`. The extra layer exists because one event can contain several entities — `contains: ["payment", "order"]` tells you which keys are present.

payment.captured and order.paid overlap

For a normal one-payment order both fire, in no guaranteed order. Pick one as your provisioning trigger and treat the other as informational, or key on `order_id` so the second one is a no-op.

Test mode has its own webhooks

Test and live mode are separate configurations with separate secrets. A webhook added in test mode does not exist in live mode, which is why the first real payment after launch often reaches nothing at all.

Answers

Razorpay webhook questions

Can I test Razorpay webhooks without real money?+

Yes. Test mode issues its own keys and test card numbers, and it fires the same webhooks with the same signature scheme against your test-mode webhook secret.

Why is my signature verification failing?+

Check that you are using the webhook secret rather than the API key secret, and that you are hashing the raw request body rather than a parsed and re-serialised object. Verify the captured request here with the same secret to isolate which of the two it is.

What is the difference between payment.authorized and payment.captured?+

`authorized` means the bank has reserved the funds; `captured` means you have taken them. If auto-capture is on, the two arrive close together. If it is off and you never capture, an authorized payment expires and the money goes back.

Why did I get the same event twice?+

Either a retry after your 200 was lost, or two overlapping events for one transaction such as `payment.captured` and `order.paid`. Store the event's `id` from the body and the entity id, and make handling idempotent — Razorpay does not promise exactly-once.

Does Razorpay send a test event when I create a webhook?+

No. There is no ping. Nothing arrives until a real event occurs, which is why the Send test webhook panel here is useful for checking your routing before you make a payment.

Other providers

Testing something else?

One click

Point Razorpay at a URL and watch the payload land.

Razorpay Dashboard → Account & Settings → Webhooks → Add New Webhook — paste the URL, trigger an event, and read exactly what arrived.

No signup · No email · Ready in about a second