razorpay-patterns-demo
Razorpay webhook idempotency + retry patterns. India-accessible mirror of stripe-payments-demo.
→ Try the live Standard Checkout demo
Endpoints
POST /api/webhook— Razorpay async webhook receiver. Verifies theX-Razorpay-Signatureheader (HMAC-SHA256 of the raw body with your webhook secret), then runs a Redis SETNX guard with a 24h TTL on the payment/order/subscription entity ID so duplicate deliveries short-circuit before the handler runs. This is the authoritative billing-state source.POST /api/create-order— Standard-Checkout order creation endpoint. Request:{ amount: paise, currency: "INR", receipt? }, response:{ order_id, amount, currency }. Exp-backoff retry on 5xx; fails fast on 4xx.POST /api/verify-payment— client-callback signature verifier. Receivesrazorpay_order_id / razorpay_payment_id / razorpay_signaturefrom Checkout.js success handler, recomputes HMAC-SHA256(order_id|payment_id,KEY_SECRET), constant-time compare. Returns 200 if verified, 400 if tampered.GET /api/health— Redis PING liveness probe.
Sequence
Razorpay -> POST /api/webhook
verify X-Razorpay-Signature (HMAC-SHA256 of raw body)
invalid -> 400
valid -> extract eventId from payload.payment.entity.id (etc.)
-> SETNX razorpay:event:{eventId} EX 86400
duplicate -> 200 { duplicate: true } [no-op]
new -> dispatch by event.type -> 200Why this exists
Razorpay retries webhooks on non-2xx for up to 24 hours. Without an idempotency guard, every retry would re-fire whatever side-effects the handler does — duplicate database writes, duplicate emails, duplicate grants of paid access. The SETNX guard is a single atomic operation that makes the whole flow exactly-once from the business logic's perspective, even though the transport is at-least-once.
The pattern is identical to Stripe's: only the signing scheme differs (Razorpay uses straight HMAC-SHA256 of the body; Stripe adds a timestamp and structured Stripe-Signature header). Both demos share lib/idempotency.ts and lib/retry.ts in spirit — copy once, adapt signature verification, done.