How to Add UPI Payment Confirmation to a Checkout Flow (UX + Engineering)
VyaparGateway Team
Payments Editorial
Adding UPI payment confirmation to a checkout flow is harder than it looks. The customer sees a QR, scans it, approves the payment in their UPI app, and then expects your website to know about it immediately. But there's no direct connection between the customer's UPI app and your website — your application learns about the payment only when the gateway's webhook arrives at your server. Bridging that asynchronous gap with a checkout UX that feels instant is the engineering problem this article solves.
The three signals you can use for confirmation
There are exactly three ways your frontend can find out that the payment landed. Pick one — or combine — based on your tech stack and reliability requirements.
- HTTP polling — the frontend asks your backend 'is intent X paid yet?' every few seconds. Simple, works everywhere, slightly wasteful on requests.
- WebSocket / Server-Sent Events — your backend pushes the status change to the frontend the instant the webhook lands. Real-time, but adds infrastructure complexity.
- Server-side webhook only — your frontend doesn't need to know in real time; the success page is reached separately (via email link, deep link from UPI app, or a manual 'check status' button).
For most checkout flows, HTTP polling is the right choice. It's simple, reliable, and the customer's perception of 'instant' only needs a 2–3 second poll interval.
The recommended state machine
A checkout flow with UPI confirmation has six distinct states. Your UI should make the current state visually obvious so the customer always knows what to do (or not do):
- summary — customer reviews the order and clicks Pay.
- intent_creating — backend creates a payment intent; show a brief spinner.
- awaiting_payment — QR rendered, polling started, 'Waiting for payment' message visible.
- paid — webhook confirmed, transition to success page with order details.
- expired — intent expired before payment, show 'Payment window expired, try again' with a regenerate button.
- failed — payment was attempted but failed (rare), show retry option and contact-support fallback.
The transitions between these states should be explicit in your component logic. Avoid 'optimistic' transitions to paid before the webhook confirms — wait for the real signal.
Building the polling endpoint
Your backend needs a lightweight endpoint that the frontend can call repeatedly. It should be fast, cacheable for a brief window, and require minimal database work:
GET /api/intents/:id/status — returns { status: 'awaiting_payment' | 'paid' | 'expired' | 'failed', utr?: string, paid_at?: number }
Implementation tips:
- Index the intent ID column for fast lookup.
- Return a 304 Not Modified if status hasn't changed (lets the frontend save bandwidth).
- Don't include sensitive data in the polling response — just the status and the UTR.
- Add a small jitter to prevent thundering-herd if many customers are polling at once.
- Stop polling once the frontend sees a terminal status (paid/expired/failed).
The 'waiting for payment' UX
While the customer is paying, your UI is their only signal that anything is happening. A good waiting state includes:
- The QR code displayed prominently, with the amount and merchant name visible above it.
- A short instruction: 'Scan with PhonePe, GPay, Paytm or any UPI app.'
- A subtle animated indicator (pulse, dots, or progress ring) so the customer knows the page is alive.
- A 'Show pay button instead' option on mobile that opens the customer's UPI app directly via intent URL.
- A visible countdown showing the QR expiry time (e.g. 'Expires in 14:32').
- A 'I have paid' button that triggers an immediate status check (in case the polling missed a moment).
- Clear fallback copy: 'If your payment is taking longer than usual, please don't pay again — contact support.'
"The 'don't pay again' line in your waiting UI prevents the single most expensive customer mistake — accidental duplicate payments because the customer panicked when nothing happened in 30 seconds."
Handling the success transition
When polling returns status: 'paid', the frontend should:
- Stop polling immediately.
- Show a clear success animation (subtle checkmark, not a confetti explosion — this is payments, not a game).
- Transition to the success page with the order ID in the URL so it's bookmarkable.
- Trigger any client-side analytics 'page_view' event (the server already fired the conversion event from the webhook).
- Display order details fetched from the DB, not from URL parameters.
Handling expiry and failure gracefully
Two failure modes you must handle:
- Intent expired — the customer didn't pay in time. Show a friendly 'Your payment window expired. Click below to try again' with a button that creates a fresh intent and restarts the flow. Don't make the customer re-enter their order.
- Payment failed in UPI app — rare but possible (insufficient funds, daily limit, network issue). Show 'Payment didn't go through. Please try again or contact support with order ID X.' Same regeneration flow.
Both failure cases should preserve the customer's order state so they can retry without losing context. Don't redirect them to a generic error page that loses cart data.
Real-time alternative: WebSockets for high-touch checkouts
If you need true sub-second confirmation (e.g. you're running a high-volume flash-sale and every second of latency loses customers), upgrade from polling to a WebSocket or Server-Sent Events channel. The pattern: when your webhook handler marks the intent paid, it publishes a message to a Redis pub/sub channel keyed by intent ID; an active WebSocket connection from the frontend subscribes to that channel and pushes the update to the browser. Latency drops from ~3 seconds (polling) to ~50ms (push). Worth the infrastructure investment only if you can measure the conversion impact. VyaparGateway helps you issue dynamic UPI QR codes, verify payments, and notify your stack via webhooks—without charging a per-transaction platform fee on top of your plan.
Frequently asked questions
- Should I use polling or WebSockets to confirm UPI payments?
- For most checkouts, HTTP polling at 2–3 second intervals is enough. It's simple to implement, requires no extra infrastructure, and feels effectively instant to the customer. Upgrade to WebSockets only if you have a measurably high-stakes checkout (flash sale, time-pressured booking) where sub-second confirmation matters.
- What should I show the customer while they're paying via UPI?
- Show the QR code with the amount and merchant name above it, an animated indicator that the page is actively waiting, a countdown to QR expiry, and a clear 'don't pay again — contact support if delayed' message. The waiting UI is your customer's only signal that the payment is in progress.
- What happens if a payment confirmation arrives after the customer closes the tab?
- Your server-side webhook still fires regardless of the customer's browser state — the order is marked paid in your DB and fulfilment proceeds. The customer reaches your success page later via the receipt email link or an account-page order lookup. Never make confirmation dependent on the customer staying on the checkout page.
- How long should the QR expiry be?
- Most VyaparGateway customers default to 10–15 minutes per intent. Shorter expiry forces customers to act quickly (good for conversion) but causes failures if they're slow to scan. Longer expiry is more forgiving but increases the chance of stale-QR issues. 15 minutes is a reasonable default; adjust based on your customer behaviour.
Free tools for Indian merchants
No sign-up, no ads, no data selling
Use our free, browser-only tools whenever you need them. We don't store the values you enter or track you across the web.
📱 UPI QR Code Generator
Create UPI / URL / WhatsApp / WiFi QRs. Export as SVG or PNG.
🧮 GST Calculator (CGST + SGST + IGST)
Add or remove GST across 5%, 12%, 18%, 28% slabs.