How the webhook URL works. It is a public address that records whatever is sent to it and
answers 200. An inbox keeps the 50 most recent requests, accepts bodies up to 256 KB, and is
deleted 24 hours after it was created — or sooner if you press Delete URL or our server
restarts, because requests are held in memory and never written to disk.
Anyone who has the URL can read what it received. Use sandbox and test-mode accounts, do not route production traffic or customer data through it, and delete it when you are done. Privacy details.
What is a Razorpay webhook?
A Razorpay webhook is an HTTP POST sent to a URL configured in the dashboard when a subscribed event occurs: a payment is authorised, captured or fails, an order is paid, a refund is processed, a subscription is charged, a payout changes state. Because many payment methods complete outside the browser, the webhook is the dependable confirmation that a payment succeeded.
The envelope names the event in event and lists the entity types it carries in contains. Each of those appears under payload as an object with a single entity member, so a payment is found at payload.payment.entity. An order.paid event contains both an order and the payment that paid it.
What a Razorpay webhook request looks like
The request line and headers as they reach your server, followed by the body. Ids, tokens and the signature are placeholders.
POST /your/webhook/path HTTP/1.1
Host: your-server.example
X-Razorpay-Signature: 0f0c1e5a4d3b2c1a9e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c8b7a6f
X-Razorpay-Event-Id: OEXAMPLE0event1
Content-Type: application/json
User-Agent: Razorpay-Webhook/v1
{
"entity": "event",
"account_id": "acc_EXAMPLE0000001",
"event": "payment.captured",
"contains": [
"payment"
],
"payload": {
"payment": {
"entity": {
"id": "pay_EXAMPLE0000001",
"entity": "payment",
"amount": 50000,
"currency": "INR",
"status": "captured",
"order_id": "order_EXAMPLE000001",
"method": "upi",
"captured": true,
"email": "ada@example.com",
"contact": "+919876543210",
"notes": {
"order_ref": "6735"
},
"fee": 1180,
"tax": 180,
"created_at": 1767225590
}
}
},
"created_at": 1767225600
}
Amounts are integers in the currency's smallest unit, so 50000 with INR is 500.00 rupees. notes returns the key-value pairs you attached when creating the order or payment and is the simplest link back to your own records. Timestamps are Unix seconds. The word entity appears at three levels with three meanings: the envelope's type, the wrapper key, and the payment's own type.
Headers Razorpay sends
| Header | Example | What it is |
|---|---|---|
X-Razorpay-Signature | 0f0c1e5a4d3b2c1a9e8d7c6b5a4f3e2d1c0b9a8f7e6… | HMAC-SHA256 of the body, hex, keyed with the webhook secret. |
X-Razorpay-Event-Id | OEXAMPLE0event1 | Unique id of the event. It is the same on every retry. |
Content-Type | application/json | The body is JSON. |
User-Agent | Razorpay-Webhook/v1 | Identifies the sender. |
Header names are case-insensitive. Your framework may show them lower-cased, and HTTP/2 always does.
How Razorpay signs a webhook
When you create a webhook you set a secret for it. Razorpay computes an HMAC-SHA256 of the raw request body with that secret and sends the hex digest in X-Razorpay-Signature. The webhook secret is unrelated to your API key secret, and using the wrong one of the two is the usual cause of a mismatch.
No timestamp is signed, so the signature alone does not stop a replay; record X-Razorpay-Event-Id and ignore an id you have already handled. Compute it over the raw request body exactly as received — before any JSON parsing — and compare with a constant-time function.
const crypto = require('crypto');
// rawBody: Buffer or string of the body exactly as received
function verifyRazorpay(rawBody, header, webhookSecret) {
const expected = crypto.createHmac('sha256', webhookSecret)
.update(rawBody).digest('hex');
const a = Buffer.from(header || '');
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Timeouts, retries and event order
Return a 2xx within a few seconds; Razorpay's documentation at the time of writing gives five. Anything else is a failure, and the event is sent again with exponential backoff for about a day. A webhook that is still failing after that period is disabled and has to be re-enabled in the dashboard.
The same event can therefore arrive several times, always with the same X-Razorpay-Event-Id. Different events for one payment can also arrive out of sequence, with payment.captured ahead of payment.authorized; decide the outcome from the payment's status, never from which event came last.
Catch a real Razorpay webhook
- Press Create webhook URL at the top of this page and copy the address.
- In the Razorpay Dashboard, switch to Test Mode and open the Webhooks section of the account settings.
- Add a webhook, paste the address as the URL, type a throwaway secret, and tick events such as
payment.captured. - Make a test payment through a test-mode checkout or payment link.
- Read the event, the signature and the event id header in the inbox above.
To run a caught request against your own code, select it and press Open in API Tester: the method, headers and body are loaded into the request console, where you enter your server's public URL and send it. A body you edit will no longer match its signature — that is the signature doing its job.
Things that catch people out
- Two different secrets. The webhook secret you typed is what signs webhooks. The API key secret signs the checkout response instead.
- Authorised is not captured. Without automatic capture, an authorised payment that is never captured is refunded later; fulfil on
payment.capturedororder.paid. - Test and live mode have separate webhooks. One configured in test mode never fires for live payments.
api-tester.com is independent and is not affiliated with Razorpay. Providers change header names, limits and retry schedules; treat the official Razorpay documentation as the source of truth.
Related examples and tools
Twitch Webhook Example
Twitch EventSub sends three kinds of message to the same URL, told apart by a header: a challenge to prove you own the endpoint, the notifications themselves, and a revocation when a subscription ends.
Zoom Webhook Example
Zoom will not save a webhook endpoint until it has answered a validation challenge with a hash made from the app's Secret Token, and it repeats that check from time to time.
WhatsApp Webhook Example
WhatsApp Cloud API webhooks come from Meta's shared webhook system: a GET challenge when you register the URL, then deeply nested JSON for every incoming message and every delivery status.
Dropbox Webhook Example
A Dropbox webhook tells you who has changes and nothing about what changed.
Asana Webhook Example
Asana hands you the signing secret instead of asking for one: it arrives once, in a header, during a handshake your endpoint has to answer.
Idempotency
Why a retried notification must not capture or ship twice, and how idempotency keys solve it.
Curl POST JSON
Send a JSON body like this one from a terminal.
Webhook Tester
The inbox on its own page, plus a guide to signatures, retries and idempotent handlers.
All Webhook Examples
30 providers side by side: content type, signature scheme and retry behaviour.
Other webhooks signed with an HMAC of the body
References
- Official Razorpay documentation — The provider's own guide to the requests described on this page.
- Razorpay Docs: validate and test webhooks — The signature check and ways to test an endpoint.
- Razorpay Docs: payments webhook events — The payment events and sample payloads.
- RFC 2104: HMAC — The keyed-hash construction this signature is built on.
- Node.js: crypto.timingSafeEqual() — The constant-time comparison used in the code on this page.
These links leave api-tester.com, which is independent of the companies and projects they point to.
Razorpay webhook FAQ
What does a Razorpay webhook payload look like?
A JSON object with entity set to event, account_id, the event name, a contains array, a payload object holding each entity under its type name with an entity member, and created_at.
How do I verify X-Razorpay-Signature?
Compute an HMAC-SHA256 of the raw request body using the webhook secret you set in the dashboard, hex-encode it and compare it with the X-Razorpay-Signature header in constant time.
How do I avoid processing a Razorpay webhook twice?
Store the value of the X-Razorpay-Event-Id header for each processed event and skip any request whose id you have already seen.
Does Razorpay retry failed webhooks?
Yes. A non-2xx response or a timeout is retried with exponential backoff for about a day according to Razorpay's documentation at the time of writing, after which the webhook is disabled.
Which Razorpay event means the payment succeeded?
payment.captured for a payment, or order.paid for an order. payment.authorized only means the funds are held and still need capturing.