[AVAILABLE],
packages/api/src/routes/webhooks-razorpay.ts, packages/api/src/webhooks/verifyRazorpayWebhookSignature.ts. Route receipt and durable dedup are proven, see the scope note in Razorpay for what’s proven where.The route
POST /webhooks/razorpay only exists if RAZORPAY_WEBHOOK_SECRET is configured, unset in
production means the route is never mounted and returns 404, mirroring how the connector
itself is absent when its own credentials are unset.
Order of operations, and why it’s in this order
The handler does exactly four things, strictly in this order, and the order is deliberate:- Verify the signature. Side-effect-free, nothing is written yet.
- Only then read the event id header.
- Only then parse the payload.
- Only then record the event, the one side-effecting step.
Verifying the signature yourself
X-Razorpay-Signature header. This matches Razorpay’s own documented webhook signature
scheme.
A working example, taken directly from the unit test’s fixture:
Required headers
The body is capped at 1MB, over that limit returns
413 {"error":"Payload too large."}.
Responses
A duplicate delivery still returns
200, Razorpay’s retry behavior expects an
acknowledgement, not an error, for an event it already successfully delivered. It is simply
never reprocessed.
How duplicate deliveries are actually prevented
RazorpayWebhookEventStore.recordIfUnseen() is a single atomic call that both checks whether
an event id has been seen and persists it, there is no separate check-then-set, so there is
no race window between two near-simultaneous deliveries of the same event. In production this
is backed by Supabase, an INSERT into razorpay_webhook_events whose primary key on
event_id is the entire atomicity mechanism. It is never in-memory outside NODE_ENV=test,
see Production deployment.
This dedups deliveries. It does not dedup processing, that’s a second, independent check
inside the settlement processor, see Settlement.
A real captured payload
packages/api/tests/fixtures/razorpay-webhook-real-delivery.ts is an actual
Razorpay-delivered payload, redacted for PII, not a synthetic test fixture. If you’re writing
your own test payloads, note the shape it proves: payload.payment and payload.refund are
siblings under payload, not nested inside each other.
schemas/requests/razorpay-webhook-event.schema.json) allows
additional properties beyond what’s shown here, the full Razorpay envelope is stored
verbatim. The schema documents shape for reference, it is not what authenticates a request,
the signature is.
Local testing without a real Razorpay account
Start the server withNODE_ENV=test (see Quickstart step 3), which mounts
the webhook route using RAZORPAY_WEBHOOK_TEST_MODE_PLACEHOLDER_SECRET,
razorpay-webhook-test-placeholder-secret, unless RAZORPAY_TEST_WEBHOOK_SECRET is set.
Compute a signature with the same HMAC-SHA256 scheme and send it yourself, copy-paste
runnable exactly as written, confirmed against a live local server:
Next
Settlement
What happens after a webhook is durably recorded.
Razorpay
The full refund connector this webhook closes the loop for.