Short answer: there are three common ways to accept UPI on a website — (A) integrate a payment service provider (PSP/payment gateway) (recommended), (B) offer UPI deep-links / intent that open the user’s UPI app on mobile, and (C) show a UPI QR code (static or dynamic). Below I give a practical step‑by‑step plan, what to implement on front and back end, sample code snippets, and important notes (security, testing, pitfalls).
- Choose approach (summary)
- PSP / Payment Gateway (Razorpay, Paytm, Cashfree, PayU, etc.): easiest for production — they handle KYC, settlements, webhooks, reconciliation, refunds, PCI, signatures. Use if you want server-side APIs, SDKs, and reliable callbacks.
- UPI deep-link / intent (mobile-only): simple to implement, opens the user’s UPI app to complete payment. You must still verify payment server-side because mobile app callbacks may be unreliable.
- UPI QR code (desktop + mobile camera): present a QR your customers scan with their UPI app. Dynamic QR lets you include amount and order id; static QR is for fixed payee.
- High-level integration steps
- Register with PSP or bank (merchant onboarding, KYC, GST/registration).
- Get test/sandbox credentials and keys.
- Decide front-end UX: button for “Pay with UPI / PhonePe/Google Pay/Paytm” or “Scan QR”.
- Create an order on your server and generate a unique transaction reference (order_id / tr).
- Generate UPI payment URI or ask PSP to create an order / checkout token.
- Present the URI/QR to user or redirect to PSP checkout.
- Receive webhook/callback from PSP (or poll transaction status) and verify server-side.
- Update order, release goods/services, and record settlements/reconciliation.
- UPI payment URI (parameters)
A UPI payment URI payload format (commonly supported):
upi://pay?pa=merchant-vpa@bank&pn=Merchant+Name&tr=ORDER12345&tn=Order+note&am=100.00&cu=INR&url=https://example.com/order/ORDER12345
Meaning:
- pa = payee VPA (merchant@bank)
- pn = payee name
- tr = transaction ref (unique)
- tn = transaction note
- am = amount (optional; include if you want amount prefilled)
- cu = currency (INR)
- url = optional callback/merchant url
- Front-end examples
a) Simple link (mobile web)
- HTML anchor that opens UPI app:
<a id="upiPay" href="upi://pay?pa=merchant@bank&pn=Merchant+Name&tr=ORDER123&am=250.00&cu=INR">Pay with UPI</a>
b) Intent (Android fallback)
- Use Android intent URI to prefer specific apps or handle fallback:
window.location = "intent://pay?pa=merchant@bank&pn=Merchant#Intent;scheme=upi;package=com.phonepe.app;end";
// or use a generic intent without package so the system shows UPI apps
Note: behavior varies by browser and platform — iOS Safari will not accept Android intent, so use the upi:// link on iOS.
c) QR code for desktop or mobile camera
- Generate a QR code whose payload is the same UPI URI string. Use any QR library (e.g., qrcodejs, QRCode.js) to render the UPI URI as a QR.
Example (vanilla JS using QRCode library conceptually):
let upiUri = "upi://pay?pa=merchant@bank&pn=Merchant+Name&tr=ORDER123&am=500.00&cu=INR";
new QRCode(document.getElementById("qrcode"), upiUri);
- Backend / server-side: order creation, verification, webhook
- Always create an order server-side and produce a unique order_id/tr. Do not rely only on client-side parameters.
- After the user pays via app/QR, verify transaction status:
- If using a PSP: verify using their transaction-status API or webhook. Validate signature HMAC or RSA signature they provide.
- If not using PSP: you must poll your acquiring bank or use UPI APIs available through your bank/integrator (this is uncommon for merchants; most use PSPs).
- Only mark order complete after server-side verification.
Example pseudo-code (Node.js/Express style) for webhook verification:
app.post('/webhook/upi', (req, res) => {
const payload = req.body;
const signature = req.headers['x-psp-signature'];
if (!verifySignature(payload, signature, PSP_SECRET)) {
return res.status(400).send('invalid signature');
}
// payload includes order_id, status, txn_id, amount
if (payload.status === 'SUCCESS') {
markOrderPaid(payload.order_id, payload.txn_id, payload.amount);
} else {
markOrderFailed(payload.order_id);
}
res.status(200).send('ok');
});
- Testing and go‑live
- Use PSP sandbox environment to run test payments and webhooks.
- Test multiple UPI apps (Google Pay, PhonePe, Paytm, BHIM) and platforms (Android Chrome, iOS Safari).
- Test failure modes, partial payments, duplicate callbacks, timeouts, and refund flows.
- After testing, move to production keys and verify settlements.
- Security & compliance checklist
- Use HTTPS everywhere.
- Store PSP/API keys securely (env vars, secrets manager).
- Verify webhook signatures.
- Use unique order IDs and do idempotent handling of webhooks/callbacks.
- Implement refunds via PSP APIs when required.
- Complete merchant KYC as required by your PSP and bank.
- Log transactions and store receipts for reconciliation and audits.
- Common pitfalls & tips
- Deep-link/intent does not guarantee a reliable client-side success callback — always verify server-side via gateway API/webhook.
- Amount/parameters can be modified at the UPI app side if user edits — server verification prevents fraud.
- Some UPI apps don’t return a browser callback; rely on PSP webhooks or server polling.
- Static QR is fine for in-person payments; for online orders use dynamic QR or deep-link so you can tie amount and order id.
- Refunds and disputes must go through your PSP/bank — plan for reconciliation delays.
- Example flow using a PSP (recommended)
- Merchant creates order on your server -> calls PSP createOrder API -> PSP returns checkout token or UPI deeplink/QR -> front-end opens PSP checkout/UPI app or shows QR -> user completes payment in UPI app -> PSP notifies your server via webhook and/or you query transaction status -> you fulfill order.
- If you want code for a specific stack or a sample using a specific PSP
- I can provide a concrete integration example (e.g., Razorpay or Cashfree) for Node.js/React or for PHP/Python — tell me which stack or PSP and I’ll give exact sample code and typical API endpoints.
If you want, I can now:
- give a copy/pasteable implementation for (a) mobile deep-link + QR front-end + Node.js webhook verification, or
- show how to integrate a specific PSP (Razorpay/Cashfree/Paytm) step-by-step with sample API calls.
Which one would you like me to provide next?