Summary
After a checkout flow completes, the frontend gets redirected back to a success page. It's tempting to treat that redirect as the signal that payment succeeded — it's immediate, it's simple, and most of the time it's even true. It's also not something a payment record should ever be built on.
Problem
A client-side redirect tells you the browser landed on a URL. It doesn't tell you money moved. The redirect can fire on a payment that failed after the browser navigated away, and because it's client-controlled, it can be hit directly without a payment ever happening at all. Any system that marks an invoice paid based on that redirect is trusting a signal the client, not the payment processor, controls.
Discussion
The fix isn't more validation on the redirect — it's not using the redirect as the source of truth at all. Razorpay (like most payment processors) sends a server-to-server webhook once a payment actually clears, signed so the receiving server can verify it wasn't forged. The redirect becomes purely a UX affordance — "here's where the user goes next" — while the webhook, with its signature checked, is what's allowed to change payment status in the database.
This does add real cost. Webhook delivery isn't instantaneous or guaranteed on the first attempt, so the UI has to handle a state where the user is looking at a success page but the backend hasn't confirmed the payment yet — usually a "processing" state that resolves shortly after. That's a harder UX problem than "redirect = done." It's the right tradeoff anyway: a payment record that lags by a few seconds but is always accurate beats one that's instant but occasionally wrong.
Key Takeaways
- A client-side redirect is a UX signal, not a payment confirmation — never let it write to a payment record.
- Verify the processor's webhook signature server-side before trusting the event it carries.
- Accept the UX cost of an eventual-confirmation state; it's cheaper than reconciling payments that were never real.