{"id":28285,"date":"2026-10-27T11:43:37","date_gmt":"2026-10-27T06:13:37","guid":{"rendered":"https:\/\/razorpay.com\/blog\/?p=28285"},"modified":"2026-10-29T11:45:26","modified_gmt":"2026-10-29T06:15:26","slug":"payment-gateway-webhooks-india","status":"publish","type":"post","link":"https:\/\/razorpay.com\/blog\/payment-gateway-webhooks-india\/","title":{"rendered":"Payment Gateway Webhooks in India: Signature Verification, Retries and Event Logs"},"content":{"rendered":"<p>A payment gateway webhook is an HTTP notification sent from the gateway to your backend when a payment-related event occurs, such as a payment being authorised, captured, failed or refunded.<\/p>\n<p>Picture a hypothetical INR 2,499 order. The customer&#8217;s payment succeeds, but your merchant endpoint is restarting behind a bad deploy. The money moved. Your fulfilment system has no record of it. Minutes later the notification arrives twice, and your signature check rejects both because a JSON middleware rewrote the body first.<\/p>\n<p>That is the everyday shape of the problem: missing updates, repeated notifications and signature mismatches leaving order, refund and subscription records inconsistent. It usually surfaces during a checkout launch, an endpoint incident or a gateway integration review.<\/p>\n<p>The engineering question is specific. How do we verify a webhook, recover a missed event and ensure the same payment does not trigger fulfilment twice? Retrying delivery can itself produce duplicates, so the application needs durable receipt, deduplication and state checks. This guide sets out the evaluation criteria, then shows how Razorpay documents signatures, retries, logs, recovery and operation-specific idempotency.<\/p>\n<div id=\"webhook-key-takeaways\" style=\"border-left: 4px solid #2878db; background: #f0f6ff; padding: 24px; margin: 28px 0; text-align: left;\">\n<h2 style=\"margin-top: 0;\">Key Takeaways<\/h2>\n<ul>\n<li style=\"margin-bottom: 16px;\"><strong>Razorpay provides signed webhooks, automatic retries and Dashboard event logs.<\/strong> Exponential-backoff delivery retries and self-service replay for eligible missed events help merchants recover from delivery failures. These controls support a recoverable integration when your application also stores events durably, monitors failures and reconciles payment status.<\/li>\n<li style=\"margin-bottom: 16px;\"><strong>Webhook reliability depends on authentication, recovery and duplicate handling.<\/strong> Compare signature validation, retry windows, acknowledgement deadlines, delivery logs and replay eligibility when evaluating a payment gateway. Razorpay documents HMAC-SHA256 authentication, retries over 24 hours from event creation and a five-second response window. The affected webhook is disabled after persistent delivery failure, so plan alerting and recovery.<\/li>\n<li style=\"margin-bottom: 16px;\"><strong>Signature verification authenticates incoming webhook payloads.<\/strong> Compare <code>X-Razorpay-Signature<\/code> with an HMAC-SHA256 digest of the original request bytes, using your configured webhook secret. Register raw-body handling before JSON middleware. Reject altered payloads and malformed signatures; keep webhook secrets separate from API credentials and scope rotation to the delivery route.<\/li>\n<li style=\"margin-bottom: 16px;\"><strong>Delivery logs and application logs answer different questions.<\/strong> The India FAQ documents Dashboard logs for deliveries from the last three hours, including request and response headers. Owner, Pseudo Owner and R&amp;D roles can view them. Your own durable inbox and worker logs establish whether the intended business action actually completed.<\/li>\n<li style=\"margin-bottom: 16px;\"><strong>Missed events can be replayed within the documented limits.<\/strong> India self-service replay covers unacknowledged events up to seven days old, with up to five replay requests per 24 hours and 500 entity IDs per specific-events request. Acknowledged events are excluded. Selected disabled URLs are enabled automatically, and replayed events use the current webhook secret.<\/li>\n<li style=\"margin-bottom: 16px;\"><strong>Duplicate prevention requires event-level and business-level checks.<\/strong> Atomically deduplicate <code>x-razorpay-event-id<\/code> within the relevant integration context, then enforce a separate business-operation constraint. Acknowledge only after durable receipt and process slower work asynchronously. Repeated or out-of-order notifications must not fulfil an order twice or move a captured payment back to an authorised state.<\/li>\n<li style=\"margin-bottom: 16px;\"><strong>Razorpay supports idempotent retries for specific API operations.<\/strong> Use <code>X-Payout-Idempotency<\/code> for supported payout operations, <code>X-Refund-Idempotency<\/code> for Normal and Instant Refunds, and <code>X-Transfer-Idempotency<\/code> for direct transfers. Reuse the same key and identical request for the same supported operation. These endpoint-specific contracts do not establish a universal idempotency header for every customer payment API.<\/li>\n<\/ul>\n<\/div>\n<h2>What Makes a Payment Gateway Webhook Reliable?<\/h2>\n<p>Evaluate payment gateway webhooks by how they authenticate events, retry failed deliveries, expose delivery history and recover missed notifications. Then check how your application handles duplicate and out-of-order events. Razorpay documents these controls, but dependable fulfilment also requires durable event storage, idempotent processing and payment-status reconciliation inside the merchant application.<\/p>\n<h3>Compare Webhook Authentication, Delivery and Recovery Controls<\/h3>\n<div class=\"webhook-table-scroll\" style=\"width: 100%; max-width: 100%; overflow-x: auto; margin: 20px 0;\" tabindex=\"0\" role=\"region\" aria-label=\"Webhook capability comparison\">\n<table style=\"width: 100%; min-width: 720px; border-collapse: collapse; table-layout: fixed;\">\n<thead>\n<tr>\n<th style=\"padding: 12px; text-align: left; overflow-wrap: anywhere;\" scope=\"col\">Evaluation criterion<\/th>\n<th style=\"padding: 12px; text-align: left; overflow-wrap: anywhere;\" scope=\"col\">Documented Razorpay mechanism<\/th>\n<th style=\"padding: 12px; text-align: left; overflow-wrap: anywhere;\" scope=\"col\">What the merchant must implement<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Authenticity<\/td>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">HMAC-SHA256 signature in <code>X-Razorpay-Signature<\/code><\/td>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Verify the raw body with the configured webhook secret before any business action<\/td>\n<\/tr>\n<tr>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Failed delivery<\/td>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Exponential backoff over 24 hours; five-second response window<\/td>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Durable intake, timely 2xx acknowledgement, monitored failures<\/td>\n<\/tr>\n<tr>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Duplicates and event order<\/td>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">At-least-once delivery, unique event ID, order not guaranteed<\/td>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Atomic event deduplication and state-aware business transitions<\/td>\n<\/tr>\n<tr>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Delivery visibility<\/td>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Dashboard delivery logs for the last three hours<\/td>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Correlate gateway history with application and worker logs<\/td>\n<\/tr>\n<tr>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Missed-event recovery<\/td>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">India self-service replay of unacknowledged events up to seven days old; Support for older events<\/td>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Repair the receiver first, observe replay limits, reconcile processing<\/td>\n<\/tr>\n<tr>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Repeated API requests<\/td>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Specific contracts for payouts, refunds and direct transfers<\/td>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Correct header, stable key, identical request for the supported operation<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n<h3>Webhooks, Browser Callbacks and API Checks Have Different Jobs<\/h3>\n<p>A browser success callback should never be your only basis for fulfilment. Razorpay&#8217;s own <a href=\"https:\/\/razorpay.com\/docs\/webhooks\/?preferred-country=IN\">webhooks overview<\/a> separates asynchronous server-to-server notification from <code>callback_url<\/code>a checkout parameter used in redirect and WebView flows. Server-side retrieval is the third job: poll selectively for unresolved or business-critical states rather than running an unbounded loop. For wider collection context, see <a href=\"https:\/\/razorpay.com\/payment-gateway\/\">Razorpay Payment Gateway<\/a>.<\/p>\n<blockquote><p><strong>DID YOU KNOW:<\/strong> A server can process an event and still lose its acknowledgement. The resulting redelivery is consistent with at-least-once delivery and does not by itself indicate that another payment occurred.<\/p><\/blockquote>\n<h2>How Do You Verify a Razorpay Webhook Signature?<\/h2>\n<p>Razorpay webhook verification uses HMAC-SHA256 with your configured webhook secret as the key and the raw HTTP request body as the message. Compare the result with <code>X-Razorpay-Signature<\/code> before parsing the payload or changing business records. Recreating JSON from a parsed object can change the bytes and invalidate the signature.<\/p>\n<h3>Preserve the Raw Body Before JSON Middleware<\/h3>\n<p>Configure the webhook route to retain the raw request body before JSON middleware runs. Validate those original bytes against <code>X-Razorpay-Signature<\/code> using the endpoint&#8217;s webhook secret, then parse the verified payload. This prevents signature failures caused by verifying a newly serialised JSON object rather than the request Razorpay signed.<\/p>\n<p>Razorpay&#8217;s <a href=\"https:\/\/razorpay.com\/docs\/webhooks\/validate-test?preferred-country=IN\">validate and test webhooks<\/a> guidance carries an explicit instruction not to parse or cast the webhook request body, and the <a href=\"https:\/\/razorpay.com\/docs\/webhooks\/faqs?preferred-country=IN\">webhooks FAQ<\/a> names the two usual causes of mismatch: a secret that differs from the one used at setup, and signature-generation logic that passes an already-encoded body.<\/p>\n<pre style=\"margin: 20px 0; padding: 20px; background: #10203a; color: #f1f6ff; border-radius: 8px; overflow-x: auto; max-width: 100%; white-space: pre; font: 15px\/1.65 ui-monospace,SFMono-Regular,Consolas,monospace;\" tabindex=\"0\" aria-label=\"Illustrative Node.js webhook receiver\"><code class=\"language-javascript\" style=\"display: block; padding: 0; margin: 0; background: transparent; color: inherit; font: inherit; white-space: inherit;\">\/\/ Illustrative receiver: supply a reviewed, durable inbox adapter.\r\nconst express = require(\"express\");\r\nconst crypto = require(\"crypto\");\r\n\r\nfunction createWebhookApp({ webhookSecret, persistVerifiedEvent }) {\r\n  if (typeof webhookSecret !== \"string\" || !webhookSecret.length) {\r\n    throw new Error(\"Configure a webhook secret before starting\");\r\n  }\r\n  if (typeof persistVerifiedEvent !== \"function\") {\r\n    throw new Error(\"A durable inbox adapter is required\");\r\n  }\r\n  const app = express();\r\n  app.post(\"\/webhooks\/razorpay\",\r\n    express.raw({ type: \"application\/json\", limit: \"1mb\" }),\r\n    async (req, res) =&gt; {\r\n      if (!Buffer.isBuffer(req.body)) return res.status(415).end();\r\n      const signature = req.get(\"X-Razorpay-Signature\");\r\n      if (!signature || !\/^[0-9a-f]{64}$\/i.test(signature)) {\r\n        return res.status(400).end();\r\n      }\r\n      const expected = crypto.createHmac(\"sha256\", webhookSecret)\r\n        .update(req.body).digest();\r\n      const supplied = Buffer.from(signature, \"hex\");\r\n      if (!crypto.timingSafeEqual(supplied, expected)) {\r\n        return res.status(400).end();\r\n      }\r\n      let event;\r\n      try { event = JSON.parse(req.body.toString(\"utf8\")); }\r\n      catch { return res.status(400).end(); }\r\n      const eventId = req.get(\"x-razorpay-event-id\");\r\n      if (!eventId || !event || typeof event.event !== \"string\") {\r\n        return res.status(400).end();\r\n      }\r\n      try {\r\n        \/\/ Resolve ONLY after commit, or confirmed prior durable receipt.\r\n        \/\/ The adapter enforces atomic uniqueness in account\/environment scope.\r\n        await persistVerifiedEvent({ eventId, event, rawBody: req.body });\r\n      } catch {\r\n        return res.status(503).end();\r\n      }\r\n      return res.status(200).end();\r\n    }\r\n  );\r\n  \/\/ Other routes may use JSON parsing after the webhook route is registered.\r\n  app.use(express.json());\r\n  return app;\r\n}\r\n\r\nmodule.exports = { createWebhookApp };<\/code><\/pre>\n<p>Supply <code>webhookSecret<\/code> from a server-side secret store or environment variable, never a source-code literal. The injected <code>persistVerifiedEvent<\/code> adapter is application code: it must commit an atomic inbox record, or confirm an existing durable record, before resolving. Bind it to the correct account and Test or Live environment. A worker should recover pending records and apply separate fulfilment safeguards. The example uses a merchant-selected 1 MB request limit; review that limit and test database failure, timeouts, and concurrent delivery before deployment. The OWASP webhook security guidance is explicit that <code>==<\/code> leaks timing information, which is why the comparison above is constant-time.<\/p>\n<h3>Use the Correct Secret and Rotation Procedure<\/h3>\n<p>The webhook secret is configured by you. Razorpay&#8217;s FAQ states directly that it does not need to be the merchant secret key issued by Razorpay, so treat it as a separate credential from your API key and from the checkout-response signature.<\/p>\n<p>Secret handling depends on the delivery route. <a href=\"https:\/\/razorpay.com\/docs\/webhooks\/validate-test\/?preferred-country=IN\">Razorpay&#8217;s validation guidance<\/a> says to use the old secret for retries of older requests after a secret change. The <a href=\"https:\/\/razorpay.com\/docs\/webhooks\/replay-events?preferred-country=IN\">India self-service replay guide<\/a> says replayed events use the current webhook secret. The support-assisted FAQ describes its own original-event validation conditions. Test the applicable route before rotating secrets, and confirm support-assisted handling with Support. The example above accepts one configured secret; it is not a complete secret-rotation implementation.<\/p>\n<h3>Troubleshoot Verification Without Bypassing It<\/h3>\n<p>Work through the wrong secret, mutated request bytes, a missing header and Test versus Live configuration, which are maintained separately. Log the failure category and correlation identifiers without storing secrets or unnecessary personal payload data. Never disable verification to make a test pass. Razorpay <a href=\"https:\/\/razorpay.com\/docs\/security\/whitelists\/?preferred-country=IN\">recommends signature validation even when its webhook IPs are allowlisted<\/a>, so the allowlist is defence in depth, not a substitute.<\/p>\n<blockquote><p><strong>PRO-TIP:<\/strong> Add a test that changes one byte in an otherwise valid payload. The signature check should reject it before any order update or queue insertion occurs.<\/p><\/blockquote>\n<h2>How Do Webhook Retries and Failure Recovery Work?<\/h2>\n<p>Razorpay treats non-2xx responses and responses outside the five-second window as failed webhook deliveries. Its documented exponential-backoff policy retries delivery over 24 hours from event creation. If failures continue, the webhook is disabled and an alert is sent. Restore the receiver, enable the webhook and reconcile missed updates through the appropriate recovery flow.<\/p>\n<h3>Understand the Retry Window and Acknowledgement Boundary<\/h3>\n<p>Razorpay&#8217;s automatic retries provide a defined recovery window for temporary delivery failures. Your receiver should validate the signature, durably record the event and return a 2xx response within five seconds. Process slower fulfilment work asynchronously. If durable storage fails, do not acknowledge success; investigate the failure and allow the documented delivery-retry mechanism to operate.<\/p>\n<p>The <a href=\"https:\/\/razorpay.com\/docs\/webhooks\/best-practices\/?preferred-country=IN\">best-practices page<\/a> describes at-least-once semantics and a scenario worth memorising: your server can accept the event, miss the response deadline, have the session marked as a timeout, and receive the same event again. The five-second rule on the <a href=\"https:\/\/razorpay.com\/docs\/payments\/dashboard\/account-settings\/webhooks\/?preferred-country=IN\">Dashboard webhooks page<\/a> is therefore a duplicate-cause, not just a performance note.<\/p>\n<p>Four retry mechanisms exist in one integration and should not be merged: gateway delivery retry, your worker&#8217;s retry after durable receipt, outbound API retry with a supported idempotency key, and subscription charge retry, where exhausted attempts move a subscription to halted in the <a href=\"https:\/\/razorpay.com\/docs\/webhooks\/subscriptions\/?preferred-country=IN\">subscription event lifecycle<\/a>. The webhook delivery documentation does not publish an exact retry timetable or a guaranteed attempt count, so do not assume one for webhook delivery.<\/p>\n<h3>Handle Disablement as an Explicit Recovery State<\/h3>\n<p>If delivery failures continue for 24 hours, Razorpay disables the affected webhook and sends an alert. Fix the endpoint and restore delivery through the Dashboard, then check for missed payment updates. Disablement is documented for the webhook; it is not evidence that the merchant account or every webhook has been disabled. Manual re-enabling is separate from the replay flow, which documents automatic enabling of selected disabled URLs.<\/p>\n<h3>Alert Before the Recovery Window Is Exhausted<\/h3>\n<p>Own your alerting. Monitor sustained delivery errors, signature-failure rate, queue depth, oldest unprocessed event age and disabled-webhook status. Thresholds belong to your engineering team; the documentation does not publish an alerting SLA. Name the on-call owner for each alert in the runbook. The alert email address configured at webhook setup is the notification channel for disablement, so keep it current.<\/p>\n<blockquote><p><strong>DID YOU KNOW:<\/strong> A 2xx delivery response only tells the gateway that your endpoint accepted the notification. Your worker can still fail afterwards, so you need processing-state logs and an internal retry mechanism.<\/p><\/blockquote>\n<h2>Where Can You Find Event Logs and Replay Missed Webhooks?<\/h2>\n<p>Razorpay&#8217;s webhook FAQ directs merchants to Developers, then Webhooks, then the selected webhook for log history. <a href=\"https:\/\/razorpay.com\/docs\/webhooks\/replay-events?preferred-country=IN\">India replay documentation<\/a> covers self-service replay of unacknowledged events up to seven days old. Use gateway history alongside your application logs to establish whether an event was delivered, durably accepted, and successfully processed.<\/p>\n<h3>Separate Delivery Logs From Application Processing Logs<\/h3>\n<p>Use the Dashboard&#8217;s webhook history to investigate delivery, then correlate the event with your receiver and worker logs. Record the event ID, event type, related entity ID, receipt time, verification result and processing outcome in your own system. A successful delivery does not establish that fulfilment finished, and a replay-eligibility window is not a log-retention policy.<\/p>\n<p>Example merchant log, not a Razorpay Dashboard export:<\/p>\n<pre style=\"margin: 20px 0; padding: 20px; background: #10203a; color: #f1f6ff; border-radius: 8px; overflow-x: auto; max-width: 100%; white-space: pre; font: 15px\/1.65 ui-monospace,SFMono-Regular,Consolas,monospace;\" tabindex=\"0\" aria-label=\"Example merchant processing log\"><code style=\"display: block; padding: 0; margin: 0; background: transparent; color: inherit; font: inherit; white-space: inherit;\">event_id: evt_EXAMPLE001\r\nevent_type: payment.captured\r\npayment_id: pay_EXAMPLE99\r\norder_id: order_EXAMPLE42\r\nreceived_at: 2026-10-20T11:04:08Z\r\nverification_result: passed\r\nreceipt_state: committed\r\nprocessing_state: completed\r\nlast_error: null\r\nprocessed_at: 2026-10-20T11:04:11Z<\/code><\/pre>\n<p><a href=\"https:\/\/razorpay.com\/docs\/webhooks\/faqs\/?preferred-country=IN\">Razorpay&#8217;s India FAQ<\/a> states that Dashboard webhook logs show deliveries from the last three hours, including request and response headers for each delivery attempt. Users with the Owner, Pseudo Owner or R&amp;D role can view them. This viewing window does not establish the backend retention period or a native export feature. Keep durable application logs for longer-term processing evidence.<\/p>\n<h3>Use the Correct Recovery Route<\/h3>\n<div class=\"webhook-table-scroll\" style=\"width: 100%; max-width: 100%; overflow-x: auto; margin: 20px 0;\" tabindex=\"0\" role=\"region\" aria-label=\"Webhook recovery options\">\n<table style=\"width: 100%; min-width: 720px; border-collapse: collapse; table-layout: fixed;\">\n<thead>\n<tr>\n<th style=\"padding: 12px; text-align: left; overflow-wrap: anywhere;\" scope=\"col\">Recovery route<\/th>\n<th style=\"padding: 12px; text-align: left; overflow-wrap: anywhere;\" scope=\"col\">Documented scope<\/th>\n<th style=\"padding: 12px; text-align: left; overflow-wrap: anywhere;\" scope=\"col\">Required qualification<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Automatic retry<\/td>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Failed delivery, exponential backoff across 24 hours from event creation<\/td>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Does not continue indefinitely after the failure window<\/td>\n<\/tr>\n<tr>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">India Dashboard self-service replay<\/td>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Unacknowledged events, date-range or specific-entity flow<\/td>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\"><a href=\"https:\/\/razorpay.com\/docs\/webhooks\/replay-events?preferred-country=IN\">Up to seven days old; five replay requests per 24 hours; up to 500 entity IDs per specific-events request<\/a>. Acknowledged events are excluded<\/td>\n<\/tr>\n<tr>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Older-event assistance<\/td>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Replay guidance routes older events to Support<\/td>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">The <a href=\"https:\/\/razorpay.com\/docs\/webhooks\/faqs\/?preferred-country=IN\">FAQ<\/a> separately describes a support-assisted route for events no older than 15 days, with original-event enablement and validation conditions. Confirm eligibility with Support; older-event recovery is not guaranteed<\/td>\n<\/tr>\n<tr>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Merchant processing recovery<\/td>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Events already accepted into your durable inbox<\/td>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Reprocess from your own records and reconcile via status APIs; this is not provider replay<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n<h3>Recover After an Endpoint Outage<\/h3>\n<ol>\n<li>Identify the affected URL, outage interval, and unresolved order or payment IDs from application records.<\/li>\n<li>Repair connectivity, raw-body verification, and durable storage, then test the receiver before requesting recovery.<\/li>\n<li>Open Developers, then Webhooks, and inspect the affected webhook&#8217;s history and status.<\/li>\n<li>Choose Replay events and select a date range covering eligible unacknowledged events up to seven days old.<\/li>\n<li>Choose unacknowledged events or specific events. For specific events, select the entity and enter up to 500 IDs per request, manually or by CSV.<\/li>\n<li>Select the intended URLs and event types. Selected disabled URLs are enabled automatically by replay.<\/li>\n<li>Submit within the limit of five replay requests per 24 hours and monitor receipt plus worker completion. Submission is not proof of completed processing.<\/li>\n<li>Reconcile unresolved records using <a href=\"https:\/\/razorpay.com\/docs\/api\/orders\/fetch-payments\/?preferred-country=IN\">Fetch Payments for an Order<\/a> and the <a href=\"https:\/\/razorpay.com\/docs\/api\/payments\/?preferred-country=IN\">Payments APIs<\/a>. Contact Support for older events.<\/li>\n<\/ol>\n<blockquote><p><strong>PRO-TIP:<\/strong> Plan reconciliation before clicking Replay. The same event ID persists across the original delivery and the replay, so keep deduplication active throughout recovery.<\/p><\/blockquote>\n<h2>How Do You Prevent Duplicate and Out-of-Order Processing?<\/h2>\n<p>Use Razorpay&#8217;s <code>x-razorpay-event-id<\/code> to recognise repeated deliveries of the same event. Persist the event atomically and make the business action idempotent as well. Different events can concern the same payment, so event-level deduplication alone cannot prevent repeated fulfilment. Handle state transitions without assuming that webhook arrival order matches event order.<\/p>\n<h3>Deduplicate Events Atomically<\/h3>\n<p>Store each verified event in a durable inbox keyed by its event ID and integration context. Enforce uniqueness in the database so concurrent deliveries cannot both create new work. Return success for an event already durably accepted, while your worker tracks pending, completed and failed processing. Use a separate fulfilment constraint for the business operation.<\/p>\n<p>Avoid a non-atomic check-then-insert. A durable inbox transaction with a worker polling pending rows removes the gap between a database write and a queue publish; a reviewed transactional-outbox design achieves the same result. Include account and environment context when several integrations share infrastructure. These are merchant architecture patterns, not managed gateway features.<\/p>\n<h3>Apply Payment-State Rules Before Fulfilment<\/h3>\n<p><code>payment.authorized<\/code> and <code>payment.captured<\/code> may arrive out of order, and the <a href=\"https:\/\/razorpay.com\/docs\/webhooks\/payments?preferred-country=IN\">payments webhook events documentation<\/a> notes that a payload is a snapshot of the entity when the event occurred, so a payment can already be captured when the authorised event lands. Never regress a captured state because an older authorised event arrives. Match the intended order, amount, and currency, and run a server-side state check when the state is unresolved. Authorisation is not settlement, and a delayed webhook is never a reason to issue a fresh payment.<\/p>\n<p>Design example: one INR 2,499 order receives the same captured event twice. Both deliveries can be accepted, but the durable event record and the fulfilment constraint permit exactly one fulfilment operation. This is an illustration, not a guarantee against every duplicate charge.<\/p>\n<blockquote><p><strong>DID YOU KNOW:<\/strong> A valid signature proves the payload passed the documented authenticity check. It does not make a repeated, previously valid event new.<\/p><\/blockquote>\n<h2>Which Razorpay APIs Support Idempotency Keys?<\/h2>\n<p>Razorpay documents operation-specific idempotency for supported payout, refund, and direct-transfer requests. These APIs use different headers and constraints. Reuse the same key and an identical request when retrying the same operation. Webhook event deduplication is a separate safeguard, and none of these contracts establishes a universal idempotency header for every payment-creation endpoint.<\/p>\n<h3>Match the Header to the Operation<\/h3>\n<div class=\"webhook-table-scroll\" style=\"width: 100%; max-width: 100%; overflow-x: auto; margin: 20px 0;\" tabindex=\"0\" role=\"region\" aria-label=\"Idempotency by API operation\">\n<table style=\"width: 100%; min-width: 720px; border-collapse: collapse; table-layout: fixed;\">\n<thead>\n<tr>\n<th style=\"padding: 12px; text-align: left; overflow-wrap: anywhere;\" scope=\"col\">Operation<\/th>\n<th style=\"padding: 12px; text-align: left; overflow-wrap: anywhere;\" scope=\"col\">Documented identifier<\/th>\n<th style=\"padding: 12px; text-align: left; overflow-wrap: anywhere;\" scope=\"col\">Scope<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Create Payout and supported Composite APIs<\/td>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\"><code>X-Payout-Idempotency<\/code><\/td>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Payout documentation states the key is mandatory since 15 March 2025, with a 4 to 36 character key reused for retries of the same pending operation<\/td>\n<\/tr>\n<tr>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Normal and Instant Refund APIs<\/td>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\"><code>X-Refund-Idempotency<\/code><\/td>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Normal and Instant Refunds: minimum 10 characters, using letters, numbers, hyphens or underscores. Retry the same operation with the same key and identical body; use a new key for a genuinely new refund<\/td>\n<\/tr>\n<tr>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Route direct transfers<\/td>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\"><code>X-Transfer-Idempotency<\/code><\/td>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Applies to the <a href=\"https:\/\/razorpay.com\/docs\/api\/payments\/route\/direct-transfers-idempotent-request\/?preferred-country=IN\">documented direct-transfer operation<\/a> only; do not extend it to every Route API<\/td>\n<\/tr>\n<tr>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Incoming webhook event<\/td>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\"><code>x-razorpay-event-id<\/code><\/td>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Provider-issued event identifier for merchant-side deduplication, not an outbound request key<\/td>\n<\/tr>\n<tr>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Customer payment creation or capture<\/td>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Verify the exact endpoint contract<\/td>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Check the collection or capture endpoint separately; payout, refund and direct-transfer keys cannot be assumed to apply. Keep order controls, status checks and fulfilment deduplication<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n<p>Idempotency-key rules vary by operation. Razorpay&#8217;s payout documentation specifies a 4 to 36 character key, while its refund documentation specifies a minimum of 10 characters. Use the contract for the endpoint you are calling rather than applying one length rule to all payment APIs.<\/p>\n<p>Refunds illustrate the point twice over: alongside the dedicated header, the <a href=\"https:\/\/razorpay.com\/docs\/api\/refunds\/create-normal\/?preferred-country=IN\">Create a Normal Refund<\/a> endpoint treats <code>receipt<\/code> as an idempotency key and rejects a reused value on the same payment.<\/p>\n<h3>Retry the Same Business Operation Safely<\/h3>\n<p>Persist the key and the intended payload before making a supported outbound request. If the response is lost, retain the same operation identity and check status. Do not manufacture a fresh key while the original payout is still processing. Different operations need distinct keys. For supported refund requests, a <code>409 Conflict<\/code> response can mean an earlier request is still processing. Retry that same refund with its original idempotency key and identical body, following the <a href=\"https:\/\/razorpay.com\/docs\/api\/refunds\/normal-refunds-idempotent?preferred-country=IN\">refund API contract<\/a>. Other operations have their own response and retry rules. A <a href=\"https:\/\/razorpay.com\/route\/\">Razorpay Route<\/a> transfer is not a RazorpayX payout, and neither is the same as collecting a new customer payment.<\/p>\n<h2>What Should You Test Before Going Live?<\/h2>\n<p>Test valid and tampered signatures, duplicate deliveries, concurrent processing, slow responses, storage failure, and out-of-order events. Then test recovery and reconciliation in staging. A passing webhook endpoint is only one part of the flow: verify that the intended business action happens once and that failures remain recoverable. These are acceptance criteria, not results from a completed test run.<\/p>\n<div class=\"webhook-table-scroll\" style=\"width: 100%; max-width: 100%; overflow-x: auto; margin: 20px 0;\" tabindex=\"0\" role=\"region\" aria-label=\"Webhook failure test cases\">\n<table style=\"width: 100%; min-width: 620px; border-collapse: collapse; table-layout: fixed;\">\n<thead>\n<tr>\n<th style=\"padding: 12px; text-align: left; overflow-wrap: anywhere;\" scope=\"col\">Test<\/th>\n<th style=\"padding: 12px; text-align: left; overflow-wrap: anywhere;\" scope=\"col\">Required result<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Valid payload and correct secret<\/td>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Event durably accepted and reaches the intended processing state<\/td>\n<\/tr>\n<tr>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Altered body, wrong secret or malformed signature<\/td>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Rejected before any business action<\/td>\n<\/tr>\n<tr>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Duplicate event, including concurrent requests<\/td>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">One durable event record, no repeated fulfilment<\/td>\n<\/tr>\n<tr>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Different events for the same business operation<\/td>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">State checks and business-operation uniqueness prevent repeated fulfilment<\/td>\n<\/tr>\n<tr>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Receiver cannot persist the event<\/td>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">No false success acknowledgement; alert and investigate<\/td>\n<\/tr>\n<tr>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Slow receiver or non-2xx response<\/td>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Document actual test delivery behaviour without claiming a full 24-hour test<\/td>\n<\/tr>\n<tr>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Worker crash after durable receipt<\/td>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Internal recovery resumes pending work safely<\/td>\n<\/tr>\n<tr>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Out-of-order authorised and captured events<\/td>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">No incorrect state regression or premature fulfilment<\/td>\n<\/tr>\n<tr>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Replay after recovery and secret rotation<\/td>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Eligible events received; current-secret self-service replay and older automatic-retry handling tested separately<\/td>\n<\/tr>\n<tr>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Outbound API timeout<\/td>\n<td style=\"padding: 12px; overflow-wrap: anywhere;\">Same key and payload for the supported operation; no blind new payment attempt<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n<figure id=\"webhook-processing-sequence\" style=\"margin: 24px 0; padding: 20px; background: #f0f6ff; border: 1px solid #c9dcf6; border-radius: 8px;\" aria-label=\"Webhook processing sequence\">\n<div style=\"display: flex; flex-wrap: wrap; gap: 12px;\">\n<div style=\"flex: 1 1 180px; padding: 16px; background: #ffffff; border: 1px solid #c9dcf6; border-radius: 6px;\"><span style=\"display: block; font-weight: bold; color: #1559a6;\">1. Gateway<\/span><span style=\"display: block;\">Sends the event<\/span><\/div>\n<div style=\"flex: 1 1 180px; padding: 16px; background: #ffffff; border: 1px solid #c9dcf6; border-radius: 6px;\"><span style=\"display: block; font-weight: bold; color: #1559a6;\">2. Receiver<\/span><span style=\"display: block;\">Verifies signature<\/span><\/div>\n<div style=\"flex: 1 1 180px; padding: 16px; background: #ffffff; border: 1px solid #c9dcf6; border-radius: 6px;\"><span style=\"display: block; font-weight: bold; color: #1559a6;\">3. Durable inbox<\/span><span style=\"display: block;\">Commits the event<\/span><\/div>\n<div style=\"flex: 1 1 180px; padding: 16px; background: #ffffff; border: 1px solid #c9dcf6; border-radius: 6px;\"><span style=\"display: block; font-weight: bold; color: #1559a6;\">4. Receiver<\/span><span style=\"display: block;\">Returns 2xx<\/span><\/div>\n<div style=\"flex: 1 1 180px; padding: 16px; background: #ffffff; border: 1px solid #c9dcf6; border-radius: 6px;\"><span style=\"display: block; font-weight: bold; color: #1559a6;\">5. Worker<\/span><span style=\"display: block;\">Applies guarded action<\/span><\/div>\n<div style=\"flex: 1 1 180px; padding: 16px; background: #ffffff; border: 1px solid #c9dcf6; border-radius: 6px;\"><span style=\"display: block; font-weight: bold; color: #1559a6;\">6. Reconciliation<\/span><span style=\"display: block;\">Resolves exceptions<\/span><\/div>\n<\/div><figcaption style=\"display: block; margin-top: 14px; font-size: 14px; line-height: 1.6;\">Acknowledges after durable receipt. Worker retries and payment-status reconciliation remain separate from gateway delivery retries.<\/figcaption><\/figure>\n<blockquote><p><strong>PRO-TIP:<\/strong> Use a controlled public HTTPS staging endpoint in Test mode. Webhooks cannot reach localhost, and current documentation lists several commonly suggested tunnelling and request-inspection domains as blocked. Check the current list before choosing one.<\/p><\/blockquote>\n<p>Recurring billing needs its own event and state checks. <a href=\"https:\/\/razorpay.com\/subscriptions\/\">Razorpay Subscriptions<\/a> emits a distinct lifecycle, and an exhausted subscription charge retry is a billing outcome, not a webhook delivery outcome.<\/p>\n<h2>Frequently Asked Questions<\/h2>\n<h3>How do I verify a Razorpay webhook signature?<\/h3>\n<p>Validate <code>X-Razorpay-Signature<\/code> using HMAC-SHA256, the configured webhook secret and the raw HTTP request body. Perform verification before parsing or acting on the payload. Re-serialising a parsed JSON object can change its bytes and break the comparison. For secret rotation, treat automatic older deliveries and self-service replay as separately documented rules.<\/p>\n<h3>How long does Razorpay retry failed webhook deliveries?<\/h3>\n<p>Razorpay documents exponential-backoff retries over 24 hours from event creation. A non-2xx response or a response outside the five-second window is a delivery failure. If failures continue, the webhook is disabled and an alert is sent to the configured address. Restore the receiver and use the appropriate recovery workflow.<\/p>\n<h3>Where can I see webhook event logs and replay missed events?<\/h3>\n<p>The FAQ directs merchants to Developers, then Webhooks, then the selected webhook for log history. Dashboard logs cover the last three hours. <a href=\"https:\/\/razorpay.com\/docs\/webhooks\/replay-events?preferred-country=IN\">India self-service replay<\/a> covers unacknowledged events up to seven days old: five requests per 24 hours and up to 500 entity IDs per specific-events request. Use application logs for processing evidence and contact Support about older events.<\/p>\n<h3>Does a duplicate webhook mean the customer was charged twice?<\/h3>\n<p>No. A duplicate webhook is usually another delivery of the same event, such as after a lost or late acknowledgement. Deduplicate <code>x-razorpay-event-id<\/code> and independently verify the underlying payment records if you suspect an additional charge. Use business-operation safeguards, so repeated notifications cannot fulfil one order twice.<\/p>\n<h3>Does Razorpay provide one idempotency key for every payment API?<\/h3>\n<p>Razorpay provides separate idempotency contracts for supported payouts, Normal and Instant Refunds, and direct transfers, with different headers and constraints. It does not establish one universal idempotency header for every customer payment-creation endpoint. Check the exact operation&#8217;s contract, reuse the key for identical supported retries, and maintain separate webhook and fulfilment deduplication.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Verify Razorpay webhook signatures, handle delivery retries, inspect event logs and recover missed events. Understand endpoint-specific idempotency and prevent duplicate processing.<\/p>\n","protected":false},"author":180,"featured_media":28291,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"inline_featured_image":false,"footnotes":""},"categories":[906],"tags":[],"class_list":{"0":"post-28285","1":"post","2":"type-post","3":"status-publish","4":"format-standard","5":"has-post-thumbnail","7":"category-payment-gateway"},"_links":{"self":[{"href":"https:\/\/razorpay.com\/blog\/wp-json\/wp\/v2\/posts\/28285","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/razorpay.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/razorpay.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/razorpay.com\/blog\/wp-json\/wp\/v2\/users\/180"}],"replies":[{"embeddable":true,"href":"https:\/\/razorpay.com\/blog\/wp-json\/wp\/v2\/comments?post=28285"}],"version-history":[{"count":5,"href":"https:\/\/razorpay.com\/blog\/wp-json\/wp\/v2\/posts\/28285\/revisions"}],"predecessor-version":[{"id":28287,"href":"https:\/\/razorpay.com\/blog\/wp-json\/wp\/v2\/posts\/28285\/revisions\/28287"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/razorpay.com\/blog\/wp-json\/wp\/v2\/media\/28291"}],"wp:attachment":[{"href":"https:\/\/razorpay.com\/blog\/wp-json\/wp\/v2\/media?parent=28285"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/razorpay.com\/blog\/wp-json\/wp\/v2\/categories?post=28285"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/razorpay.com\/blog\/wp-json\/wp\/v2\/tags?post=28285"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}