developer
How to Accept UPI in Headless Commerce: Next.js, Medusa & Shopify
Learn how to build a high-performance headless checkout in Next.js 15, Medusa.js, and Shopify Storefront API with native direct UPI intents and webhooks.
Modern direct-to-consumer (D2C) brands and tech-first retailers are rapidly moving away from monolithic platforms toward Headless Commerce. By pairing a high-performance frontend framework like Next.js 15 (React 19) with modular backend engines like Medusa.js or the Shopify Storefront GraphQL API, engineering teams achieve instantaneous sub-second page loads and bespoke checkout experiences.
However, integrating Indian payment methods into headless architectures often introduces challenges. Traditional gateway SDKs (like Razorpay standard checkout) rely on client-side modal iframes that degrade performance and disrupt custom headless design systems.
Here is the comprehensive engineering architecture guide for implementing a pure headless UPI checkout in Next.js 15 and Medusa.js with zero third-party commissions.
The Headless Commerce Revolution in India
In high-growth e-commerce, every 100 milliseconds of page latency directly reduces checkout conversion:
Monolithic Storefront (Traditional):
Browser ──► Heavy Server Template Engine ──► Legacy Database ──► Third-Party Iframe Modal (Slow, Bloated)
Headless Storefront (Modern Next.js 15 + Edge):
Browser ──► Static Edge HTML / React Server Components ──► GraphQL / REST ──► Native UPI Intent (Sub-Second)
By decoupling the frontend, your engineering team can design custom checkout modals that match your brand identity while keeping bundle sizes small.
The Challenge: Native UPI in Decoupled Frontends
When implementing UPI in a headless environment, two requirements must be satisfied:
- Device-Aware Intent Resolution: On iOS and Android mobile browsers, clicking “Pay” must invoke the OS-level URI scheme (
upi://pay?...) to launch Google Pay or PhonePe directly without intermediary redirects. - Asynchronous Order State Synchronization: The frontend needs a real-time reactive trigger (such as WebSockets or Server-Sent Events) to transition the checkout screen to “Order Confirmed” the millisecond the bank webhook arrives.
System Architecture: Medusa.js, VyaparGateway & Next.js 15
[Next.js 15 App Router Frontend]
│ (1. POST /api/checkout/initiate)
▼
[Medusa.js Backend (v2) / Node Engine]
│ (2. Calls VyaparGateway REST API)
▼
[VyaparGateway Engine] ──► Returns { qrCode, intentUrl, orderId }
│
▼
[Next.js displays Dynamic QR on Desktop / Launches Intent on Mobile]
│
▼ (Customer Authenticates PIN in PhonePe / GPay)
[Direct Real-Time Bank Settlement (T+0)]
│
▼ (3. Bank Webhook)
[Medusa.js Webhook Handler] ──► Completes Cart & Invoices Order
│
▼ (4. WebSocket / SSE Event: `payment.authorized`)
[Next.js Frontend transitions to /order/confirmed Screen]
Medusa.js Custom Payment Processor Plugin (TypeScript)
In Medusa.js v2, custom payment providers are implemented by extending the AbstractPaymentProcessor class:
import {
AbstractPaymentProcessor,
PaymentProcessorContext,
PaymentProcessorError,
PaymentProcessorSessionResponse,
PaymentSessionStatus,
} from "@medusajs/medusa";
import axios from "axios";
export default class VyaparUpiPaymentProcessor extends AbstractPaymentProcessor {
static identifier = "vyapar-upi";
protected readonly apiKey: string;
protected readonly baseUrl: string;
constructor(container: any, options: any) {
super(container);
this.apiKey = options.apiKey || process.env.VYAPAR_API_KEY;
this.baseUrl = "https://api.vyapargateway.com/v1";
}
// 1. Initialize Payment Session when Cart is created
async initiatePayment(
context: PaymentProcessorContext
): Promise<PaymentProcessorError | PaymentProcessorSessionResponse> {
const { amount, currency_code, resource_id } = context;
try {
const response = await axios.post(
`${this.baseUrl}/orders/create`,
{
amount: amount, // Medusa stores in minor units (divide by 100 if needed)
orderId: `MEDUSA_${resource_id}`,
currency: currency_code.toUpperCase(),
},
{ headers: { Authorization: `Bearer ${this.apiKey}` } }
);
return {
session_data: {
vyapar_order_id: response.data.orderId,
qr_code_url: response.data.qr_image_url,
intent_url: response.data.upi_intent_url,
},
};
} catch (err: any) {
return {
error: "Failed to initiate Vyapar UPI transaction",
code: "VYAPAR_INIT_ERROR",
};
}
}
// 2. Query status from VyaparGateway
async getPaymentStatus(
paymentSessionData: Record<string, unknown>
): Promise<PaymentSessionStatus> {
const orderId = paymentSessionData.vyapar_order_id as string;
const res = await axios.get(`${this.baseUrl}/orders/${orderId}`, {
headers: { Authorization: `Bearer ${this.apiKey}` },
});
if (res.data.status === "SUCCESS") return PaymentSessionStatus.AUTHORIZED;
if (res.data.status === "FAILED") return PaymentSessionStatus.ERROR;
return PaymentSessionStatus.PENDING;
}
}
Next.js 15 App Router Checkout Component
Here is a clean React Server / Client component pair in Next.js 15 (app/checkout/upi/page.tsx):
"use client";
import { useEffect, useState } from "react";
import Image from "next/image";
import { useRouter } from "next/navigation";
interface UpiCheckoutProps {
orderId: string;
amount: number;
qrUrl: string;
intentUrl: string;
}
export default function HeadlessUpiCheckout({
orderId,
amount,
qrUrl,
intentUrl,
}: UpiCheckoutProps) {
const router = useRouter();
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
// Detect if client is a mobile device
const userAgent = navigator.userAgent || navigator.vendor;
if (/android|iphone|ipad|ipod/i.test(userAgent.toLowerCase())) {
setIsMobile(true);
}
// Set up SSE / Polling listener for instant payment verification
const pollInterval = setInterval(async () => {
const res = await fetch(`/api/checkout/status?orderId=${orderId}`);
const data = await res.json();
if (data.status === "PAID") {
clearInterval(pollInterval);
router.push(`/order/success?orderId=${orderId}&utr=${data.utr}`);
}
}, 2500);
return () => clearInterval(pollInterval);
}, [orderId, router]);
return (
<div className="flex flex-col items-center justify-center p-8 bg-white rounded-3xl shadow-xl max-w-md mx-auto">
<h2 className="text-2xl font-bold text-slate-900">Scan to Pay via UPI</h2>
<p className="text-slate-600 mt-1 text-sm">Amount: ₹{amount.toLocaleString("en-IN")}</p>
{isMobile ? (
<div className="mt-6 w-full">
<a
href={intentUrl}
className="flex items-center justify-center w-full py-4 bg-blue-600 text-white font-semibold rounded-2xl shadow-lg active:scale-95 transition-transform"
>
⚡ Pay via Google Pay / PhonePe / Paytm
</a>
</div>
) : (
<div className="mt-6 p-4 border border-slate-200 rounded-2xl">
<Image
src={qrUrl}
alt="Dynamic UPI QR Code"
width={240}
height={240}
priority
className="rounded-lg"
/>
<p className="text-center text-xs text-slate-400 mt-3 font-mono">
Auto-expires in 04:59
</p>
</div>
)}
</div>
);
}
By connecting your Next.js 15 frontend directly to VyaparGateway, you maintain total design freedom, deliver lightning-fast checkout experiences, and collect customer payments with 0% transaction fees directly into your corporate bank account.
Direct answers
Frequently asked questions
- Why do high-growth brands choose headless commerce over monolithic platforms?
- Headless commerce separates the presentation frontend (built with Next.js or Remix) from the backend commerce database (Medusa.js or Shopify). This yields sub-second page loads, higher Google Lighthouse scores, custom checkout layouts, and full developer control.
- Can Medusa.js accept Indian UPI payments natively?
- Medusa.js comes with Stripe and manual payment modules by default. To accept UPI payments with 0% commissions, you can install or write a lightweight custom Medusa payment processor plugin that interacts with VyaparGateway.
- How does headless Shopify handle custom UPI checkouts?
- With headless Shopify, the cart and catalog live in Next.js using the Shopify Storefront GraphQL API. When the buyer clicks 'Checkout', your Next.js application initiates a direct UPI payment intent via VyaparGateway, captures the bank webhook, and creates the finalized order in Shopify via the Admin API.
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.