12 November 2026 · 4 min read
How to take UPI payments in a Next.js app with Razorpay
Most Indian buyers prefer to pay with UPI. Razorpay Checkout shows UPI next to cards and net banking, so you do not need a separate UPI integration. This guide shows the whole flow in a Next.js App Router app, using the official razorpay package.
The flow has four steps, and the order matters:
- Your server creates an order with the amount.
- The browser opens Checkout for that order.
- The buyer pays. Checkout gives the browser three values.
- Your server verifies them before it unlocks anything.
Never skip step 4. Anyone can call your success page with made-up values.
Before you start
- Create a Razorpay account and generate test keys (they start with
rzp_test_). Use test keys until you have finished testing and Razorpay has approved your account for live payments. - Put the keys in environment variables, never in the code:
RAZORPAY_KEY_ID=rzp_test_xxxxxxxx
RAZORPAY_KEY_SECRET=xxxxxxxx
The Key ID is public (the browser needs it). The Key Secret must only ever live on the server.
- Install the package:
npm install razorpay.
Step 1: create the order on the server
Amounts are in paise, as whole numbers. Rs. 499 is 49900. Work out the price on the server, from your own database. Never trust an amount sent by the browser.
// app/api/checkout/route.ts
import Razorpay from "razorpay";
export async function POST(req: Request) {
const { planSlug } = await req.json();
const amountPaise = await priceInPaiseFor(planSlug); // your own lookup, includes GST if you charge it
const razorpay = new Razorpay({
key_id: process.env.RAZORPAY_KEY_ID!,
key_secret: process.env.RAZORPAY_KEY_SECRET!,
});
const order = await razorpay.orders.create({
amount: amountPaise,
currency: "INR",
receipt: `plan-${planSlug}-${Date.now()}`,
});
// Save the order in your database now, with status "created", so you can match the payment later.
return Response.json({ orderId: order.id, amount: order.amount, keyId: process.env.RAZORPAY_KEY_ID });
}
Step 2: open Checkout in the browser
Load Razorpay's script and open Checkout with the order id.
"use client";
import Script from "next/script";
export function PayButton({ planSlug }: { planSlug: string }) {
async function pay() {
const res = await fetch("/api/checkout", { method: "POST", body: JSON.stringify({ planSlug }) });
const order = await res.json();
const checkout = new (window as any).Razorpay({
key: order.keyId,
order_id: order.orderId,
amount: order.amount,
currency: "INR",
name: "Your product",
handler: async (response: { razorpay_payment_id: string; razorpay_order_id: string; razorpay_signature: string }) => {
await fetch("/api/checkout/verify", { method: "POST", body: JSON.stringify(response) });
window.location.href = "/dashboard";
},
});
checkout.open();
}
return (
<>
<Script src="https://checkout.razorpay.com/v1/checkout.js" strategy="lazyOnload" />
<button onClick={pay}>Pay</button>
</>
);
}
UPI appears in the Checkout window when it is enabled for your Razorpay account. You can check the payment methods in your Razorpay dashboard settings.
Step 3 and 4: verify the signature
When the payment works, Checkout calls handler with a payment id, the order id and a signature. The signature is an HMAC (a keyed fingerprint) of order_id|payment_id, made with your Key Secret. Only Razorpay and you know the secret, so if you can reproduce the signature, the values are genuine.
// app/api/checkout/verify/route.ts
import crypto from "node:crypto";
export async function POST(req: Request) {
const { razorpay_order_id, razorpay_payment_id, razorpay_signature } = await req.json();
const expected = crypto
.createHmac("sha256", process.env.RAZORPAY_KEY_SECRET!)
.update(`${razorpay_order_id}|${razorpay_payment_id}`)
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(String(razorpay_signature));
const ok = a.length === b.length && crypto.timingSafeEqual(a, b);
if (!ok) return Response.json({ error: "Bad signature" }, { status: 400 });
// Mark the order as paid in your database. Use the order id you saved in step 1.
return Response.json({ ok: true });
}
Two details worth copying: the comparison is constant time (timingSafeEqual), and the order id comes from your records, not only from the browser.
Do not rely on the browser alone
A buyer can pay and then close the tab before handler runs. If your server only learns about payments from the browser, you will have paid customers with no access. The fix is a webhook: Razorpay calls your server directly when a payment is captured. Treat the webhook as the source of truth and the browser step as a quick shortcut. Both should be safe to run twice. We explain this in Razorpay webhooks: signature checking and idempotency.
Testing with test keys
- In test mode no real money moves. Razorpay's documentation lists the test card numbers and test UPI details to use. Follow those, because they can change.
- Test one successful payment, one failed payment, and one where you close the window halfway.
- After each test, look at your database. The order should be "paid" exactly once.
A short checklist
- Amounts in paise, calculated on the server.
- Keys in environment variables. Key Secret never reaches the browser.
- Signature verified before unlocking anything.
- A webhook as a safety net.
- Tested with test keys before going live.
- If you charge GST, add it on the server and keep it separate from your price: see how GST works on software sales.
India SaaS Kit already includes this flow, together with a demo mode so you can try a purchase without Razorpay, and GST invoices. Look at the plans if you would rather not build it yourself.