Skip to main content
Back to blog
BillingRazorpayReliability

Why Razorpay webhooks — not browser callbacks — are our billing source of truth

Prabhix Engineering · 28 June 2025 · 6 min read

Payment flows have a classic failure mode: the user pays successfully, closes the browser tab before the redirect completes, and your system never records the payment. The user is charged; your database says 'pending'. Support tickets follow.

Our billing module treats Razorpay webhooks as the single source of truth for payment state — not the browser callback.

**Order creation is server-side only.** Amounts, plan IDs, and seat counts are computed on the server from the organization's subscription record. The client never sends a price.

**Browser callback verifies signatures but doesn't mutate state.** When the user returns from Razorpay's checkout, we verify `HMAC-SHA256(order_id|payment_id, secret)` and show a confirmation UI. But we don't activate entitlements from this path alone.

**Webhooks drive state transitions.** Every Razorpay webhook hits `POST /api/v1/billing/webhooks/razorpay`. We verify `X-Razorpay-Signature` against the webhook secret, persist the raw payload to an `webhook_events` table, and process it idempotently on `event.id`.

This pattern closes three gaps:

1. **Lost redirects** — the webhook arrives regardless of browser behaviour.

2. **Duplicate processing** — idempotency on `event.id` means retries are safe.

3. **Audit trail** — raw webhook payloads are retained for dispute resolution and debugging.

Entitlements (feature flags, seat limits) are updated only after webhook-confirmed payment. The UI may show 'processing' for a few seconds after checkout — that's intentional. Correctness beats instant gratification.