> ## Documentation Index
> Fetch the complete documentation index at: https://razorpay.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Integration Health: Webhooks and Fetch APIs

> Build one state machine fed by webhooks and fetch APIs, with the scenarios, gaps and failures to test.

Available in
🇮🇳 India

Webhooks are your source of truth; fetch APIs are your safety net. Build one state machine that both feed.

**Test it:** subscribe to the events your method pages list. Verify the `X-Razorpay-Signature` header on every webhook, store event IDs and skip repeats and handle out-of-order and duplicate events. Confirm your reconciler recovers a missed webhook by calling `GET /v1/orders/:id/payments`.

## Which events to subscribe to

**Bare-minimum events**

| Event | What to do |
| - | - |
| `payment.captured` or `order.paid` | The payment is captured: fulfil the order. `order.paid` carries both the order and the payment. Required. |
| `payment.failed` | Mark the attempt as failed and keep the order open. Required. |
| `payment.authorized` | Capture the payment if you capture manually. Required if manual capture. |
| `refund.processed`, `refund.failed` | Update the order and tell the customer. Required. |

**Recommended events**

| Event | What to do |
| - | - |
| `payment.downtime.started`, `updated`, `resolved` | Hide or restore a method. See [Downtime Handling](https://razorpay.com/docs/payments/payment-gateway/s2s-integration/test-s2s/best-practices/downtime-handling.md). |
| `token.confirmed`, `rejected`, `paused`, `cancelled` | Recurring only. Tested on Card recurring and UPI Autopay. On Card recurring, customer-driven pause and cancel are coming soon in Test Mode; cancelling through the Delete Token API works today. |
| `payment.dispute.created` | Pause fulfilment and gather evidence. |

**Handle every event correctly**

* Reply `2xx` within 5 seconds, then process the event in the background.
* Check that the webhook came from Razorpay: recompute `X-Razorpay-Signature` over the raw body before parsing.
* Ignore duplicates using the `x-razorpay-event-id` header.
* Do not rely on the order of events. Never move an order back from `paid`.

**When webhooks are down**

| What happened | What Razorpay does | What you do |
| - | - | - |
| Your endpoint fails or times out | Retries for 24 hours, then disables the webhook and emails you | Fix the endpoint, re-enable the webhook, then fetch every order from the outage window |
| Webhooks are delayed | Delivers them late | Your reconciler fetches orders still pending after 15 minutes |

## Webhook or fetch?

| You need to | Use | Why |
| - | - | - |
| Decide when to fulfil | Webhook: `payment.captured` or `order.paid` | It is pushed to you as soon as the payment is captured. |
| Know an attempt failed | Webhook: `payment.failed`. Fallback: `GET /v1/orders/:id/payments` | Mark the attempt failed and keep the order open. |
| Capture, if you capture manually | Webhook: `payment.authorized`. Fallback: `GET /v1/payments/:id` | Capture inside your capture window, or it is refunded. |
| Track a refund | Webhooks: `refund.processed`, `refund.failed`. Fallback: `GET /v1/payments/:id/refunds` | Update the order and tell the customer. |
| Show the customer a result after the callback | Verify the signature; fetch the payment once if unsure | The webhook may land a few seconds after the customer returns. |
| Catch up on a payment with no webhook after 15 minutes | Fetch: `GET /v1/orders/:id/payments` | Covers delayed or failed webhooks. |
| Recover after your endpoint was down | Fetch every order from the outage window | Webhooks retried for 24 hours may have been disabled. |
| Reconcile every day | Fetch payments and settlements by date | Confirms your books match Razorpay's. |

**Design principles:** one state machine, two inputs — webhooks and fetches update the same order record through the same code. Forward-only — ignore any update that would move a payment or order back; accept `failed` → `authorized` as a late authorisation. Back off — fetch a pending order a few times with growing gaps, then leave it to the reconciler. Idempotent — processing the same update twice changes nothing.

**APIs you'll use**

| Purpose | API | Available on test mode |
| - | - | - |
| Create an order | `POST /v1/orders` | Available |
| Create a payment | `POST /v1/payments/create/json` (redirect: `/create/redirect`) | Available |
| Submit or resend OTP | `otp_submit` / `otp_resend` URLs in `next[]` | Available (cards) |
| Fetch a payment | `GET /v1/payments/:id` | Available |
| Fetch an order's payments | `GET /v1/orders/:id/payments` | Available |
| Capture | `POST /v1/payments/:id/capture` | Coming soon (cards) |
| Refund | `POST /v1/payments/:id/refund` | Available |
| Fetch downtimes | `GET /v1/payments/downtimes` | Coming soon |
| Recurring: tokens and charges | `GET /v1/customers/:id/tokens`, `POST /v1/payments/create/recurring` | Available |
| Settlements | `GET /v1/settlements` | Live only |

## Scenarios to test your integration

| Scenario ID | Area | Scenario | Trigger | Steps to follow | Test mode |
| - | - | - | - | - | - |
| BP-19 | Setup | Subscribe to the bare-minimum events | Dashboard → Webhooks (Test Mode) | Subscribe to `order.paid` or `payment.captured`, `payment.failed`, `refund.processed`, `refund.failed` (and `payment.authorized` if you capture manually). Run a success and failure trigger on a method page and confirm each event arrives. | Available |
| BP-01 | Fulfilment | Fulfil on the webhook, not the callback | Any success trigger | Pay, then drop the callback (close the tab or skip your handler). Confirm the order is fulfilled once, from the webhook. | Available |
| BP-03 | Handling | Duplicate webhook | Replay a stored event | Replay the same event. Confirm it is processed once, keyed on `x-razorpay-event-id`. | Available |
| BP-30 | Fulfilment | Customer closes the browser before the callback | Any success trigger | Same as BP-01: the webhook (or a fetch) still drives fulfilment. | Available |
| BP-04 | Outage | Endpoint down, then recovery | Make your endpoint return `500` | Return `500` for a few minutes and make two payments. Run the reconciler; confirm both orders settle. Restore the endpoint; confirm late retries are ignored as duplicates. | Available |
| BP-05 | Fallback | No webhook: fetch fallback | Unsubscribe from `order.paid`/`payment.captured` | Pay with a success trigger. Confirm your reconciler resolves it with `GET /v1/orders/:id/payments`, with back-off. | Available |
| BP-31 | Any | Webhook delayed or missed | Unsubscribe from the event | Same as BP-05: fetch `GET /v1/orders/:id/payments` after 15 minutes and act on the result. | Available |
| BP-20 | Reconciler | Reconcile pending orders after 15 minutes | Orders left `created` and `attempted` | Leave one order unpaid and fail one payment. Run the reconciler: it fetches both after 15 minutes and fulfils, waits or closes each. | Available |
| BP-10 | Handling | Events arrive out of order | Replay `payment.failed` after `order.paid` | Replay the older event last. Confirm the order stays `paid`. | Available |
| BP-11 | Handling | Reply within 5 seconds | Make your handler sleep 10 seconds | Confirm you reply `2xx` within 5 seconds and process the event in the background. | Available |
| BP-21 | Callback | Show a result after the callback | A success trigger, webhook delayed | Verify the signature in the callback. If unsure, fetch the payment once (`GET /v1/payments/:id`) and show the result. Confirm fulfilment still waits for the webhook or the reconciler. | Available |
| BP-22 | Design | One state machine, forward-only | Feed the same update by webhook and by fetch | Process a webhook, then the same state from a fetch. Confirm nothing changes the second time, and a paid order never moves back. Confirm `failed` → `authorized` is accepted (late authorisation). | Available |
| BP-23 | Reconciler | Daily reconciliation | Fetch payments and settlements by date | Fetch yesterday's payments and settlements. Confirm your records match. | Live only |

## Scenario details

**BP-01 — Fulfil on the webhook, not the callback**

*Before you start:* webhook subscribed to `order.paid` (or `payment.captured`) and `payment.failed`.

*How to test:* create an order and pay it with a success trigger. Do not run your callback handler (as if the customer closed the browser). Wait for the webhook.

*Expected result:* the order is fulfilled once, from the webhook (`payment.captured`, `order.paid`).

*Your integration should:* treat the callback as a hint — verify the signature and show a result, but fulfil only from the webhook or a fetch.

**BP-03 · BP-10 — Duplicate and out-of-order webhooks**

*Before you start:* a stored `order.paid` and an earlier `payment.failed` for the same order.

*How to test:* replay `order.paid` with the same `x-razorpay-event-id`. Replay the earlier `payment.failed` after `order.paid`.

*Expected result:* nothing changes — the duplicate is ignored and the order stays `paid`.

*Your integration should:* key processing on `x-razorpay-event-id`; move states forward only.

**BP-04 — Endpoint down, then recovery**

*Before you start:* a reconciler you can run on demand.

*How to test:* make your endpoint return `500`. Pay two orders: one success, one failure. Run the reconciler. Restore the endpoint and let retries arrive.

*Expected result:* both orders reach the right state from the reconciler; later retries are ignored.

*Your integration should:* after a real outage, re-enable the webhook on the Dashboard and fetch every order from the outage window.

*Live mode differs:* Razorpay retries for 24 hours, then disables the webhook.

**BP-05 · BP-20 — Fetch fallback and the reconciler**

*Before you start:* a reconciler you can run on demand; no webhook subscription for the success event.

*How to test:* pay one order with a success trigger. Leave a second order unpaid; fail a payment on a third. Run the reconciler: it fetches `GET /v1/orders/:id/payments` for orders still `created`/`attempted` after 15 minutes.

*Expected result:* the paid order is fulfilled; the other two stay open, or are closed by your own timeout.

*Your integration should:* back off between fetches and never poll in a tight loop.

## Webhooks: What Fires, and What It Lets You Decide

Every scenario above delivers a specific set of events, in a specific order. This table is the one place that says which, and — the part that actually matters — what each one entitles you to do. Most integration bugs that reach production are not missing webhooks; they are acting on the wrong one.

| Scenario | What it is | Events, in order | What it lets you decide |
| - | - | - | - |
| BP-19 | Subscribe to the bare-minimum events | `order.paid`, `payment.captured`, `payment.failed`, `refund.processed`, `refund.failed`, `payment.authorized` | The order is settled and will not accept another payment: close the cart and stop any retry loop, and money is yours — this is the event to fulfil on. This attempt is over: the error reason tells you whether to offer the same method again or move the customer to another one. The money has left Razorpay: this is when you confirm the refund to the customer. The refund did not go through: it needs a retry or a manual payout, it will not resolve itself. Money is held, not yet yours: do not fulfil on this alone. |
| BP-01 | Fulfil on the webhook, not the callback | `payment.captured`, `order.paid` | Money is yours. This is the event to fulfil on. The order is settled and will not accept another payment: close the cart and stop any retry loop. |

**Before you trust any of them**

* Verify the signature on every event. Replay one event to your endpoint and send one with a changed body. The replay must be ignored and the tampered event rejected. An endpoint that skips this is an open write to your order state.
* Expect duplicates. The same event can arrive more than once. Key your handler on the event ID and make it idempotent.
* Order is not guaranteed across events. Do not assume `payment.captured` lands before `order.paid`; handle whichever arrives first.
* Webhook first, callback second. The browser callback can be lost, blocked or forged. Fulfil on the webhook.

## Common integration gaps to avoid

| Gap | What goes wrong in production | What to do | Test with |
| - | - | - | - |
| Shipping on the callback | Uncaptured payments are refunded after you have shipped. | Fulfil on `order.paid`. | BP-01 |
| Slow webhook endpoint | The webhook is disabled after 24 hours of failures. | Reply `2xx` right away and queue the work. | BP-11 |
| No duplicate check | The order is fulfilled twice. | Check `x-razorpay-event-id`. | BP-03 |
| Not re-enabling after an outage | Every later payment needs the reconciler. | Alert on the disable email; re-enable and backfill. | BP-04 |
| Test webhooks assumed in live | No events arrive after go-live. | Set up live-mode webhooks again. | BP-19 |
| No reconciler | Payments made during a webhook outage are never recorded. | Fetch orders still `created`/`attempted` after 15 minutes. | BP-20 |
| Polling in a tight loop | You hit rate limits and still miss the result. | Back off; leave the rest to the reconciler. | BP-05 |
| Two code paths for webhook and fetch | The same payment is processed differently, or twice. | One state machine, forward-only and idempotent. | BP-22 |

## Simulate failures

**Available in test mode**

| Failure | How to simulate | Expected handling | Scenario |
| - | - | - | - |
| Endpoint returns 5xx | Return `500` | Reconciler settles; late retries ignored | BP-04 |
| Slow endpoint | Sleep 10 seconds | `2xx` within 5 seconds | BP-11 |
| Duplicate event | Replay an event | Processed once | BP-03 |
| Out-of-order event | Replay an older event last | State does not move back | BP-10 |
| Tampered event | Change the body | Rejected | BP-37 |
| Webhook missing | Unsubscribe from the event | Fetch fallback with back-off | BP-05 |
| Callback before webhook | Delay your webhook handler | Fetch once to show the result; fulfil later | BP-21 |

**Not available in test mode**

| Failure | Why not | How to cover it |
| - | - | - |
| Webhook disabled after 24 hours of failures | Not simulated | Alert on the email; re-enable and backfill |
| Settlement mismatches | Settlements are live only | Daily reconciliation in live (BP-23) |

### Related Information

* [Best Practices](https://razorpay.com/docs/payments/payment-gateway/s2s-integration/test-s2s/best-practices)
* [Payment States and Late Authorisation](https://razorpay.com/docs/payments/payment-gateway/s2s-integration/test-s2s/best-practices/payment-states-and-late-authorisation.md)
* [Orders](https://razorpay.com/docs/payments/payment-gateway/s2s-integration/test-s2s/orders.md)
* [Webhooks](https://razorpay.com/docs/webhooks)
