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.

VT VyaparGateway Team Payments & Compliance 2 min read
How to Integrate Direct UPI Payments in Next.js 15 / React 19 (App Router) guide
nextjs 15 upi payment gateway react server components upi checkout nextjs payment webhook react 19 fullstack VyaparGateway

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 native crypto.timingSafeEqual provides superior constant-time guarantees.
  • Environment Variables: Set VYAPAR_API_KEY and VYAPAR_WEBHOOK_SECRET in your Vercel Project Settings or Docker .env configuration.

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.