developer
How to Integrate Direct UPI Payments in Next.js 15 / React 19 (App Router)
Complete full-stack Next.js 15 and React 19 tutorial for direct UPI integration. Learn Server Actions, dynamic QR rendering, and Route Handlers for signed webhooks.
The release of Next.js 15 and React 19 has fundamentally transformed full-stack web engineering. With the maturation of React Server Components (RSC), Server Actions, and streamlined Route Handlers, building an e-commerce checkout in Next.js no longer requires a separate Node.js backend.
Here is the complete, full-stack implementation to accept direct-to-bank UPI payments in Next.js 15 with zero third-party gateway commissions.
The Modern Next.js 15 Payment Architecture
Direct Answer: In Next.js 15 App Router, payment creation is handled via a secure Server Action (
createOrderAction), keeping API secrets off the client. The dynamic QR code and UPI intent string are generated on the server and streamed into an interactive Client Component, while payment status confirmations are handled by an edge Route Handler (app/api/webhook/route.ts).
[ User clicks "Checkout" ] ──► Invokes Server Action: createOrderAction()
│
▼ (Server-Side Only)
[ Calls VyaparGateway REST API ]
│
▼
[ Next.js Streams Checkout UI ] ◄── Returns { paymentUrl, qrCode, upiString }
│
▼ (Customer Pays via GPay / PhonePe)
[ VyaparGateway sends Webhook ] ──► app/api/webhook/route.ts
│
▼ (HMAC Verified)
[ Updates Prisma / Drizzle DB ]
Step 1: Server Action for Secure Order Creation
Create app/actions/payment.ts:
'use server';
export interface CreateOrderResult {
success: boolean;
orderId?: string;
upiString?: string;
paymentUrl?: string;
qrDataUrl?: string;
error?: string;
}
export async function createOrderAction(amount: number, clientTxnId: string): Promise<CreateOrderResult> {
const API_KEY = process.env.VYAPAR_API_KEY;
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://yourstore.com';
try {
const res = await fetch('https://vyapargateway.com/api/v1/create_order', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': API_KEY!,
},
body: JSON.stringify({
amount,
client_txn_id: clientTxnId,
p_info: `Order ${clientTxnId}`,
redirect_url: `${SITE_URL}/checkout/success`,
callback_url: `${SITE_URL}/api/webhook`,
}),
});
const data = await res.json();
if (!data.status) {
return { success: false, error: data.message || 'Order creation failed' };
}
return {
success: true,
orderId: data.data.order_id,
upiString: data.data.upi_string,
paymentUrl: data.data.payment_url,
qrDataUrl: data.data.qr_image_url,
};
} catch (err: any) {
return { success: false, error: err.message };
}
}
Step 2: Rendering the Checkout UI (RSC + Client Island)
Create app/checkout/page.tsx:
'use client';
import { useState } from 'react';
import { createOrderAction, CreateOrderResult } from '../actions/payment';
export default function CheckoutPage() {
const [loading, setLoading] = useState(false);
const [order, setOrder] = useState<CreateOrderResult | null>(null);
async function handlePay() {
setLoading(true);
const txnId = `ORD_${Date.now()}`;
const result = await createOrderAction(1999.00, txnId);
setOrder(result);
setLoading(false);
}
return (
<div className="max-w-md mx-auto p-6 bg-white rounded-2xl shadow-xl border">
<h2 className="text-xl font-bold mb-4">Express UPI Checkout</h2>
<p className="text-gray-600 mb-6">Total Due: ₹1,999.00 (Zero Surcharge)</p>
{!order ? (
<button
onClick={handlePay}
disabled={loading}
className="w-full bg-blue-600 text-white font-semibold py-3 rounded-xl hover:bg-blue-700 transition"
>
{loading ? 'Generating Dynamic QR...' : 'Pay via UPI'}
</button>
) : order.success ? (
<div className="text-center space-y-4">
<img src={order.qrDataUrl} alt="Scan UPI QR" className="mx-auto w-64 h-64 border rounded-xl" />
<p className="text-sm text-gray-500">Scan with GPay, PhonePe, Paytm, or BHIM</p>
<a
href={order.upiString}
className="block w-full bg-green-600 text-white font-semibold py-3 rounded-xl hover:bg-green-700"
>
Pay on Mobile App (1-Tap)
</a>
</div>
) : (
<p className="text-red-500">{order.error}</p>
)}
</div>
);
}
Step 3: App Router Webhook Route Handler (app/api/webhook/route.ts)
In Next.js 15, route handlers read raw text buffers using await request.text():
import { NextRequest, NextResponse } from 'next/server';
import crypto from 'crypto';
export async function POST(req: NextRequest) {
const secret = process.env.VYAPAR_WEBHOOK_SECRET!;
const signatureHeader = req.headers.get('x-webhook-signature');
if (!signatureHeader) {
return NextResponse.json({ error: 'Missing signature' }, { status: 400 });
}
// 1. Capture exact raw payload text
const rawBody = await req.text();
// 2. Validate HMAC-SHA256 signature
const expectedSig = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
const isValid = crypto.timingSafeEqual(
Buffer.from(signatureHeader),
Buffer.from(expectedSig)
);
if (!isValid) {
return NextResponse.json({ error: 'Invalid HMAC signature' }, { status: 401 });
}
// 3. Process verified event
const event = JSON.parse(rawBody);
if (event.status === 'SUCCESS') {
console.log(`Order ${event.client_txn_id} confirmed via bank UTR ${event.utr}!`);
// Execute database fulfillment (Prisma / Drizzle)
}
return NextResponse.json({ received: true });
}
Deploying on Vercel or Self-Hosted VPS
- Edge vs Node.js Runtime: Keep the webhook route handler on
export const runtime = 'nodejs';because Node’s nativecrypto.timingSafeEqualprovides superior constant-time guarantees. - Environment Variables: Set
VYAPAR_API_KEYandVYAPAR_WEBHOOK_SECRETin your Vercel Project Settings or Docker.envconfiguration.
Explore our full Next.js Integration Guide for production boilerplate and webhooks.
Direct answers
Frequently asked questions
- How do React Server Actions simplify payment processing in Next.js 15?
- React Server Actions allow order creation and secret API key authentication to execute entirely on the server without creating boilerplate REST routes, keeping sensitive gateway credentials completely off the client-side bundle.
- How should payment webhooks be implemented in the Next.js App Router?
- Webhooks are implemented using Route Handlers (app/api/webhook/route.ts), reading request.text() to capture raw payload bytes for HMAC-SHA256 signature verification before database insertion.
- Can Next.js 15 render UPI dynamic QR codes without client-side JavaScript?
- Yes. By generating the Base64 QR code or SVG string inside a React Server Component, the barcode renders as static HTML from the initial server paint with zero client hydration overhead.
Build your payment flow
Explore the API and browser-only merchant tools.
Create UPI checkout orders, verify signed events, or test the free calculators and generators without exposing credentials.