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.
Payment links, subscriptions, and the same webhook
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.