Client Area

UPI Payment Integration for Indian Businesses — PhonePe, Paytm, Razorpay UPI Intent

ByDomain India Team·DomainIndia Engineering
6 min read24 Apr 20265 views
# UPI Payment Integration for Indian Businesses — PhonePe, Paytm, Razorpay UPI Intent
TL;DR
UPI is India's dominant payment method — 40%+ of all digital payments. Most Indian websites should accept UPI. This guide covers the three practical integration paths: Razorpay (fastest to ship, covers all UPI apps), Paytm Business (good for Paytm-heavy audiences), and static UPI Intent links (zero-fee but manual reconciliation).
## The three UPI integration paths
PathSetup timeFeesReconciliationBest for
Razorpay1-2 days2% + GSTAutomatic webhookMost businesses
Paytm Business2-3 days1.99%+Automatic webhookPaytm-strong audience
PhonePe Business3-5 daysVariesAutomaticLarge B2B volumes
Static UPI link (upi://)5 minutes0%Manual (from bank statement)Low volume, freelancers, donations
## Path 1 — Razorpay (recommended for most) Razorpay is the standard for Indian SaaS. Full UPI support including UPI Intent (tap the app icon), collect requests, and autopay for subscriptions. ### Step 1 — Setup
1
Sign up at razorpay.com (KYC: PAN, GST, bank statement)
2
Dashboard → Settings → API Keys → Generate Test Key (start here) and Live Key (after KYC)
3
Note: rzp_live_XXX and secret_XXX
4
Install SDK:
### Step 2 — Create an order (server-side) **PHP:** ```php use RazorpayApiApi; $api = new Api(getenv('RAZORPAY_KEY_ID'), getenv('RAZORPAY_KEY_SECRET')); $order = $api->order->create([ 'receipt' => 'order_' . time(), 'amount' => 50000, // ₹500 in paise 'currency' => 'INR', 'notes' => ['user_id' => 42], ]); echo json_encode([ 'orderId' => $order['id'], 'key' => getenv('RAZORPAY_KEY_ID'), ]); ``` **Node.js:** ```javascript const Razorpay = require('razorpay'); const rzp = new Razorpay({ key_id: process.env.RAZORPAY_KEY_ID, key_secret: process.env.RAZORPAY_KEY_SECRET, }); app.post('/api/order', async (req, res) => { const order = await rzp.orders.create({ amount: 50000, // paise currency: 'INR', receipt: `order_${Date.now()}`, notes: { userId: req.user.id }, }); res.json({ orderId: order.id, key: process.env.RAZORPAY_KEY_ID }); }); ``` ### Step 3 — Client-side checkout (web) ```html Pay ₹500 ``` ### Step 4 — Verify signature server-side **Critical:** never trust client-provided `payment_id`. Always verify signature. **PHP:** ```php $attributes = [ 'razorpay_order_id' => $_POST['razorpay_order_id'], 'razorpay_payment_id' => $_POST['razorpay_payment_id'], 'razorpay_signature' => $_POST['razorpay_signature'], ]; try { $api->utility->verifyPaymentSignature($attributes); // Signature valid — mark order paid in DB markOrderPaid($attributes['razorpay_order_id'], $attributes['razorpay_payment_id']); } catch (RazorpayApiErrorsSignatureVerificationError $e) { http_response_code(400); echo 'Invalid signature'; } ``` **Node.js:** ```javascript const crypto = require('crypto'); app.post('/api/verify', (req, res) => { const { razorpay_order_id, razorpay_payment_id, razorpay_signature } = req.body; const expected = crypto .createHmac('sha256', process.env.RAZORPAY_KEY_SECRET) .update(`${razorpay_order_id}|${razorpay_payment_id}`) .digest('hex'); if (expected !== razorpay_signature) { return res.status(400).send('Invalid signature'); } // Mark paid await db.order.update({ where: { id: razorpay_order_id }, data: { status: 'paid' } }); res.json({ ok: true }); }); ``` ### Step 5 — Webhook (the reliable path) The handler above works for most payments, but browser can close mid-payment. Always configure webhook as the source of truth. Razorpay dashboard → Settings → Webhooks → Add: - URL: `https://yourcompany.com/webhook/razorpay` - Events: `payment.captured`, `payment.failed` - Secret: generate random 32-char string **Handler (Node.js):** ```javascript app.post('/webhook/razorpay', express.raw({ type: 'application/json' }), // must be raw for signature async (req, res) => { const sig = req.headers['x-razorpay-signature']; const expected = crypto .createHmac('sha256', process.env.RAZORPAY_WEBHOOK_SECRET) .update(req.body) .digest('hex'); if (expected !== sig) return res.status(400).send('Invalid'); const event = JSON.parse(req.body); if (event.event === 'payment.captured') { const p = event.payload.payment.entity; await markOrderPaid(p.order_id, p.id, p.amount); } res.json({ ok: true }); } ); ``` Webhooks are the canonical source of truth — handle even if the browser never comes back. ## Path 2 — Paytm Business Paytm has deeper penetration in Tier-2/3 cities. Their SDK is similar to Razorpay; refer to [business.paytm.com](https://business.paytm.com) docs. Fees: 1.99% on UPI, slightly cheaper than Razorpay for high volumes. ## Path 3 — Static UPI Intent link (zero-fee option) For low-volume, personal, or donation use cases. No gateway fees, but manual reconciliation. Create a link that opens any UPI app: ``` upi://pay?pa=yourname@hdfcbank&pn=Your%20Name&am=500&cu=INR&tn=Invoice+1234 ``` Parameters: - `pa` = payee VPA (UPI ID) - `pn` = payee name (URL-encoded) - `am` = amount (optional; user can edit) - `cu` = currency (INR) - `tn` = note / reference Generate QR code server-side: ```python import qrcode data = "upi://pay?pa=yourname@hdfcbank&pn=Your%20Name&am=500&cu=INR&tn=Invoice+1234" img = qrcode.make(data) img.save('upi-qr.png') ``` Mobile users tap the link, pick their UPI app, confirm. You reconcile from your bank statement — no automated webhook. ### Verifying payments (manual) For a business where UPI reconciliation matters: - Use ICICI/HDFC/Kotak collect-payment APIs (instant SMS → parse reference) - Or a lightweight service like **Decentro** / **Setu** — webhook on every credit to your UPI ID ## Subscriptions (recurring billing) Razorpay + UPI AutoPay supports mandates up to ₹15,000/day (NPCI limit). ```javascript // Create subscription const sub = await rzp.subscriptions.create({ plan_id: 'plan_XXX', customer_notify: 1, total_count: 12, // monthly × 12 notes: { userId: 42 }, }); ``` Customer approves the UPI mandate once; subsequent charges auto-debit. Critical: handle `subscription.charged` webhook, grant/deny access based on status. ## Refunds **Full refund:** ```javascript await rzp.payments.refund(payment_id, { speed: 'normal', // or 'optimum' for faster }); ``` **Partial refund:** ```javascript await rzp.payments.refund(payment_id, { amount: 20000, // refund ₹200 from ₹500 payment }); ``` UPI refunds hit the customer's bank in 3-5 days typically. ## Common pitfalls ## FAQ
Q Which gateway is cheapest?

For UPI specifically: Paytm Business (1.99%) < Razorpay (2%) < Stripe (doesn't support UPI directly). For cards: all are similar (~2%).

Q Can I skip gateways with a direct NPCI integration?

Only banks can be direct NPCI participants. Even Flipkart / Amazon use gateways. Accept the 2%.

Q Does this work on DomainIndia shared hosting?

Yes — both server-side SDK (PHP/Node.js) and webhook endpoints work on shared cPanel/DirectAdmin/Plesk with SSL.

Q Razorpay or Stripe for Indian customers with international cards?

Razorpay handles Indian cards + UPI. Stripe handles international cards better. Many Indian SaaS companies use Razorpay (INR) + Stripe (USD) together.

Q How do I refund if customer disputes on phone?

Server-side rzp.payments.refund(id) — always initiated by you, not by customer app. UPI refund reaches their bank within 3-5 working days.

UPI payments on a DomainIndia-hosted website — webhook-ready in under a day. See our web hosting plans

Was this article helpful?

Your feedback helps us improve our documentation

Still need help? Submit a support ticket