UPI Integration Cheatsheet for Backend Engineers
For: A backend engineer at a Series A Indian SaaS or e-commerce company who has been handed a Jira ticket to integrate UPI payments and is now drowning in NPCI docs, PSP-specific SDKs, and contradictory Medium posts — trying to understand which flow to implement, how refunds actually work, and what happens when a payment is 'pending' for 90 minutes
If you only remember one thing from this page: UPI's pending status is a settlement state, not a loading spinner. It can silently expire after ~30 minutes with no webhook, and if your order logic treats pending as "almost success," you will double-fulfill orders or strand customer money. Everything below is written around that fact.
This is a working reference for backend engineers integrating UPI through a PSP (Razorpay, Cashfree, PayU, Juspay, PhonePe PG, etc.) into an Indian SaaS or e-commerce backend. It assumes you have a merchant account and API keys and are trying to figure out which UPI payment flow to ship, how the webhooks actually behave, and which errors to retry.
1. Pick the right UPI flow
| Flow | How it works | Best for | Gotchas |
|---|---|---|---|
| Collect | You push a collect request to a VPA. User approves in their UPI app. | Web checkout where user types VPA; recurring reminders | Highest failure rate. VPA typos. User ignores notification. 2–5 min timeout typical. |
| Intent | You open a UPI deep link (upi://pay?...) that hands off to the user's UPI app. | Mobile web and native apps | Desktop won't work. You lose the return-to-app callback if user swipes away. |
| QR (Static/Dynamic) | Render a QR encoding the same UPI URI. User scans with any UPI app. | Desktop web, POS, invoices, kiosks | Static QRs don't carry a txn ID — reconcile by amount+timestamp, which is fragile at scale. Use dynamic QR. |
| UPI AutoPay (e-mandate) | One-time mandate approval, then debit on schedule. | Subscriptions, SaaS billing, EMI | Mandate approval is its own state machine. Debits still fail on insufficient balance — build dunning. |
Decision rule
- Mobile-first checkout → Intent with Collect as fallback.
- Desktop checkout → Dynamic QR with Collect as fallback.
- Subscriptions → AutoPay, never recurring Collects.
- In-store / invoice → Dynamic QR.
2. The payment state machine (the part everyone gets wrong)
Regardless of PSP, the underlying NPCI states you care about are:
| State | Meaning | Your order should be... |
|---|---|---|
CREATED / INITIATED | You created the transaction, user hasn't acted | Cart locked, order not created |
PENDING / DEEMED | Debited from payer but credit to merchant not confirmed. Legally distinct from success. | Do not fulfill. Order in AWAITING_SETTLEMENT. |
SUCCESS | Credit confirmed to merchant account by NPCI | Fulfill. |
FAILURE | Terminal failure | Release cart, allow retry with new txn ID. |
EXPIRED | Timeout (Collect ~2–5 min, Intent ~5–9 min, QR varies) | Same as failure. But verify before releasing. |
Why pending is dangerous
- A pending txn can flip to success up to T+2 business days in edge cases (bank RRN reconciliation).
- NPCI may not send a follow-up webhook on silent expiry. You must poll.
- If you mark the order failed and the money later credits, you owe the customer a refund they didn't ask for.
- If you mark the order success and it fails, you shipped goods for free.
Rule of thumb
Never derive order state from a webhook alone. On any terminal-looking event, call the PSP's fetch payment status API and treat that response as the source of truth. Webhooks are triggers; the status API is truth.
3. Webhook handling checklist
- Verify signature. Every PSP signs webhooks with HMAC-SHA256 of the raw body. Verify against raw bytes, not the parsed JSON.
- Idempotency. Store
(event_id, payment_id, status). Duplicate webhooks are normal — expect 2–5 for a single payment. - Respond 2xx fast. Ack within 5 seconds. Do work async. PSPs will retry aggressively if you 5xx or time out, which multiplies your idempotency load.
- Order state transitions must be monotonic. Never let a
SUCCESSorder go back toPENDINGbecause a stale webhook arrived out of order. Compare event timestamps. - Reconciliation job. Cron every 15 minutes: for every payment in
PENDINGolder than 20 min, hit the status API. Do not wait for a webhook that may never arrive.
4. UPI failure codes: retryable vs terminal
These are the NPCI response codes your PSP will surface (sometimes renamed). Handle by category, not by code.
| Code | Meaning | Action |
|---|---|---|
U30, ZM | Debit failed at payer bank | Terminal. Show "try another method." |
U69, Z9 | Insufficient funds | Terminal for this attempt. Prompt retry with different account. |
U16, UX | Risk / fraud decline | Terminal. Do not auto-retry. |
U67, BT | Timeout at PSP/bank | Retryable — but only with a new txn ID. Status may still resolve; check before retry. |
U54 | Transaction expired | Terminal. |
XB, XD, XF | Format / invalid VPA | Terminal. Validate input. |
U28, U88 | Beneficiary bank offline / NPCI downtime | Retryable after backoff. Consider fallback to card/netbanking. |
5. Refunds
- UPI refunds are async. The refund API returns
PROCESSEDorPENDING— the actual credit to payer arrives via a separate refund webhook, usually within minutes but occasionally up to T+2. - You cannot refund a
PENDINGpayment. Resolve to success or failure first. - Partial refunds are supported but the payer bank may reject if the original RRN is stale (rare, but budget for it).
- Store the refund RRN separately from the payment RRN. Customer support will ask for both.
- Reversal ≠ refund. A reversal happens when a debit succeeds but credit fails — NPCI auto-reverses. You get a webhook; do not treat it as a customer-initiated refund in your ledger.
6. Testing what sandbox won't show you
PSP sandboxes give you deterministic success/failure. Production gives you:
- Payments that sit in pending for 45+ minutes then succeed.
- Webhooks delivered out of order (failure event after success event).
- Duplicate charge attempts if user retries in a second UPI app before your txn expires.
- Bank downtime windows (SBI, HDFC maintenance) where U28 spikes for hours.
Before you ship: run a load test that injects delayed webhooks, out-of-order events, and duplicate payment attempts against your order service. If your fulfillment logic doesn't survive that, it won't survive week one in production. Teams building payment-heavy backends — we've seen this on GimBooks and Cashpo — spend more time on reconciliation than on the happy path, and that's the correct ratio.
7. A minimum viable integration checklist
- Signed webhook receiver with idempotency key on
event_id. - Order state machine with an explicit
AWAITING_SETTLEMENTstate that blocks fulfillment. - Status-fetch call as source of truth on every terminal-looking webhook.
- Reconciliation cron for payments pending > 20 minutes.
- Failure-code classifier (retryable vs terminal vs risk).
- Refund webhook handler, separate from payment webhook handler.
- Structured logs keyed by
merchant_txn_idandnpci_rrn. - Alerting on: webhook lag > 60s, pending rate > baseline, U28 spike (bank outage).
Frequently Asked Questions
What's the difference between UPI Collect and UPI Intent from a backend perspective?
Collect is a pull — you push a request to a VPA and wait for the user to approve in their app. Intent is a handoff — you generate a upi:// deep link and the user's UPI app takes over. Backend state handling is nearly identical; the difference is failure rate (Collect is worse) and where the timeout lives (Collect: your server; Intent: the UPI app).
How long should I keep a UPI payment in pending before marking it failed?
Don't mark it failed based on time alone. Poll the PSP's status API. A pragmatic pattern: after 20 minutes of pending, poll every 5 minutes for up to 2 hours, then escalate to a manual reconciliation queue. Never auto-release inventory or refund on timeout without a definitive status response.
Do I need to integrate with NPCI directly?
No, and you almost certainly shouldn't. Direct NPCI integration requires being a PSP or a TPAP, which involves bank sponsorship and regulatory compliance. Use Razorpay, Cashfree, Juspay, PayU, or similar — they abstract NPCI and handle settlement.
How do I handle a customer claiming they were debited but the order failed?
Ask for the UPI reference number (RRN) from their bank SMS. Look it up via your PSP's search API against the RRN, not your internal order ID. If the PSP shows the payment as failed but the customer's bank shows a debit, NPCI will auto-reverse within T+2. If it doesn't, raise a chargeback via your PSP dashboard.
Should I build UPI AutoPay for subscriptions or use recurring Collect requests?
AutoPay. Recurring Collects require the user to approve every single charge, which kills conversion. AutoPay uses an e-mandate: one approval, scheduled debits. It's a different integration surface (mandate creation, mandate state machine, debit notifications 24 hours before charge) but it's the only production-viable subscription flow on UPI.
Found this useful? CodeNicely publishes engineering and product playbooks weekly. Browse the archive or tell us what you're building.
_1751731246795-BygAaJJK.png)