How to Add Razorpay to Next.js (2026): Complete Guide with Code
Live demo: your-demo-url.vercel.app GitHub repo: github.com/your-handle/razorpay-nextjs-2026
TL;DR — The 7-Step Version
- Create a Razorpay account, switch to test mode, copy your andtext
key_id.textkey_secret - Store them in . Never commit them.text
.env.local - Build a route that creates an order on Razorpay's server.text
/api/order - Load the Razorpay checkout script on your client component.
- Open the checkout popup with the you got back.text
order_id - Verify the returned signature on your server using HMAC SHA256.
- Set up a webhook for the cases where the user closes the browser before redirect.
What You'll Build
Prerequisites
- Node.js 20 or later (Razorpay's SDK uses native fetch)
- Next.js 15.x project with the App Router
- A Razorpay account (free, no KYC needed for test mode)
- Basic familiarity with TypeScript and async/await
npx create-next-app@latest razorpay-demo --typescript --app --tailwind
cd razorpay-demo
Step 1 — Create a Razorpay Account and Get Your API Keys
- Make sure the toggle in the top-right says Test Mode (not Live).
- Go to Account & Settings → API Keys.
- Click Generate Test Key.
- Copy both the Key ID (starts with ) and the Key Secret.text
rzp_test_
Step 2 — Install the Razorpay SDK
npm install razorpay
npm install --save-dev @types/razorpay
Step 3 — Configure Environment Variables (the Secure Way)
.env.local# .env.local
RAZORPAY_KEY_ID=rzp_test_YourKeyHere
RAZORPAY_KEY_SECRET=YourSecretHere
NEXT_PUBLIC_RAZORPAY_KEY_ID=rzp_test_YourKeyHere
RAZORPAY_WEBHOOK_SECRET=AnyStrongRandomString
- Only the is exposed to the client. That's why it has thetext
key_idprefix. The secret stays server-only.textNEXT_PUBLIC_ - Do not put the secret behind . I've seen this in production code on GitHub. It leaks your secret to every visitor.text
NEXT_PUBLIC_ - Add totext
.env.local(Next.js does this by default, but double-check).text.gitignore - The webhook secret is something you make up — we'll wire it into Razorpay's dashboard in Step 7.
Step 4 — Create the Order API Route
app/api/order/route.tsimport { NextRequest, NextResponse } from "next/server";
import Razorpay from "razorpay";
const razorpay = new Razorpay({
key_id: process.env.RAZORPAY_KEY_ID!,
key_secret: process.env.RAZORPAY_KEY_SECRET!,
});
export async function POST(req: NextRequest) {
try {
const { amount, currency = "INR", receipt } = await req.json();
if (!amount || amount < 100) {
return NextResponse.json(
{ error: "Amount must be at least 100 paise (₹1)" },
{ status: 400 }
);
}
const order = await razorpay.orders.create({
amount, // in paise — ₹499 = 49900
currency,
receipt: receipt || `rcpt_${Date.now()}`,
});
return NextResponse.json({ orderId: order.id, amount: order.amount });
} catch (err) {
console.error("Razorpay order creation failed:", err);
return NextResponse.json(
{ error: "Could not create order" },
{ status: 500 }
);
}
}
Optional: The Server Actions Approach (Next.js 15)
// app/actions/payment.ts
"use server";
import Razorpay from "razorpay";
const razorpay = new Razorpay({
key_id: process.env.RAZORPAY_KEY_ID!,
key_secret: process.env.RAZORPAY_KEY_SECRET!,
});
export async function createOrder(amount: number) {
if (!amount || amount < 100) throw new Error("Invalid amount");
const order = await razorpay.orders.create({
amount,
currency: "INR",
receipt: `rcpt_${Date.now()}`,
});
return { orderId: order.id, amount: order.amount };
}
await createOrder(49900)/api/orderStep 5 — Build the Checkout Component
app/checkout/page.tsx"use client";
import { useState } from "react";
import Script from "next/script";
declare global {
interface Window {
Razorpay: any;
}
}
export default function CheckoutPage() {
const [loading, setLoading] = useState(false);
const handlePayment = async () => {
setLoading(true);
// 1. Create the order on the server
const res = await fetch("/api/order", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ amount: 49900 }), // ₹499
});
const { orderId } = await res.json();
// 2. Open the Razorpay checkout
const options = {
key: process.env.NEXT_PUBLIC_RAZORPAY_KEY_ID,
amount: 49900,
currency: "INR",
name: "Your Store Name",
description: "Pro Plan — Monthly",
order_id: orderId,
handler: async (response: any) => {
// 3. Verify on the server
const verify = await fetch("/api/verify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(response),
});
const result = await verify.json();
if (result.success) {
window.location.href = "/thank-you";
} else {
alert("Payment verification failed. Contact support.");
}
},
prefill: {
name: "Test User",
email: "test@example.com",
contact: "9999999999",
},
theme: { color: "#0f172a" },
};
const rzp = new window.Razorpay(options);
rzp.open();
setLoading(false);
};
return (
<>
<Script src="https://checkout.razorpay.com/v1/checkout.js" />
<main className="flex min-h-screen items-center justify-center">
<button
onClick={handlePayment}
disabled={loading}
className="rounded-lg bg-slate-900 px-8 py-3 text-white"
>
{loading ? "Loading..." : "Pay ₹499"}
</button>
</main>
</>
);
}
- Use Next.js , not a rawtext
<Script>tag. Next.js handles loading order properly.text<script> - The runs on success. It receivestext
handler,textrazorpay_payment_id, andtextrazorpay_order_id. You send all three to your verify endpoint.textrazorpay_signature - Don't trust the handler alone. If the user closes their browser between paying and the handler firing, you'll think the payment failed when it actually succeeded. That's what webhooks (Step 7) are for.
Step 6 — Verify the Payment Signature
app/api/verify/route.tsimport { NextRequest, NextResponse } from "next/server";
import crypto from "crypto";
export async function POST(req: NextRequest) {
const { razorpay_order_id, razorpay_payment_id, razorpay_signature } =
await req.json();
const body = `${razorpay_order_id}|${razorpay_payment_id}`;
const expectedSignature = crypto
.createHmac("sha256", process.env.RAZORPAY_KEY_SECRET!)
.update(body)
.digest("hex");
const isAuthentic = expectedSignature === razorpay_signature;
if (!isAuthentic) {
return NextResponse.json({ success: false }, { status: 400 });
}
// TODO: Save payment to your database here
// await db.payment.create({ data: { ... } })
return NextResponse.json({ success: true });
}
order_id|payment_idStep 7 — Handle Razorpay Webhooks (the Step Most Tutorials Skip)
Set Up the Webhook Endpoint
app/api/webhook/route.tsimport { NextRequest, NextResponse } from "next/server";
import crypto from "crypto";
export async function POST(req: NextRequest) {
const body = await req.text(); // raw body, not parsed
const signature = req.headers.get("x-razorpay-signature");
const expectedSignature = crypto
.createHmac("sha256", process.env.RAZORPAY_WEBHOOK_SECRET!)
.update(body)
.digest("hex");
if (signature !== expectedSignature) {
return NextResponse.json({ error: "Invalid signature" }, { status: 400 });
}
const event = JSON.parse(body);
switch (event.event) {
case "payment.captured":
// Mark the order as paid in your database
await markOrderPaid(event.payload.payment.entity);
break;
case "payment.failed":
await markOrderFailed(event.payload.payment.entity);
break;
case "refund.processed":
await markRefundProcessed(event.payload.refund.entity);
break;
}
return NextResponse.json({ received: true });
}
async function markOrderPaid(payment: any) {
// your DB logic
}
async function markOrderFailed(payment: any) {
// your DB logic
}
async function markRefundProcessed(refund: any) {
// your DB logic
}
Register It in the Razorpay Dashboard
- Go to Settings → Webhooks.
- Click + Add New Webhook.
- URL: .text
https://yourdomain.com/api/webhook - Secret: paste the same value you put in .text
RAZORPAY_WEBHOOK_SECRET - Select events: ,text
payment.captured,textpayment.failed.textrefund.processed
ngrok http 3000Step 8 — Save the Payment to Your Database
model Payment {
id String @id @default(cuid())
razorpayOrderId String @unique
razorpayPaymentId String?
amount Int
currency String @default("INR")
status String // 'created' | 'paid' | 'failed' | 'refunded'
userId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
import { prisma } from "@/lib/prisma";
// inside the POST handler, after isAuthentic === true:
await prisma.payment.upsert({
where: { razorpayOrderId: razorpay_order_id },
update: { razorpayPaymentId: razorpay_payment_id, status: "paid" },
create: {
razorpayOrderId: razorpay_order_id,
razorpayPaymentId: razorpay_payment_id,
amount: 49900,
status: "paid",
},
});
upsertStep 9 — Implement Refunds
app/api/refund/route.tsimport { NextRequest, NextResponse } from "next/server";
import Razorpay from "razorpay";
const razorpay = new Razorpay({
key_id: process.env.RAZORPAY_KEY_ID!,
key_secret: process.env.RAZORPAY_KEY_SECRET!,
});
export async function POST(req: NextRequest) {
const { paymentId, amount } = await req.json();
try {
const refund = await razorpay.payments.refund(paymentId, {
amount, // in paise; omit for full refund
speed: "normal", // or 'optimum' for instant (extra fee)
});
return NextResponse.json({ success: true, refundId: refund.id });
} catch (err: any) {
return NextResponse.json(
{ error: err.message },
{ status: 500 }
);
}
}
Step 10 — Going Live: Test Mode → Live Mode Checklist
- Complete Razorpay KYC. Submit PAN, GST (if applicable), and bank details. Approval takes 1-3 business days.
- Generate live API keys under the same Settings → API Keys page (with the toggle on Live).
- Update environment variables in production — Vercel, Netlify, or wherever you deploy. Do not commit live keys to your repo.
- Whitelist your production domain in the Razorpay dashboard under Settings → Configuration. Live mode rejects payments from unapproved domains.
- Re-register your webhook with the live-mode URL (the dashboard has separate webhook lists for test and live).
- Switch the checkout script if you hardcoded test branding anywhere.
- Test with a real ₹1 payment first. Don't trust your test-mode logs.
- Set up Slack/email alerts for webhook events so you catch issues early.text
payment.failed
Common Errors and How to Fix Them
BAD_REQUEST_ERROR: amount is requiredSignature verification failedkey_idkey_secretorder_id|payment_idRazorpay is not defined<Script>strategy="afterInteractive"window.Razorpayreq.text()req.json()TypeScript Types for Razorpay
@types/razorpaytypes/razorpay.d.tsinterface RazorpayOptions {
key: string;
amount: number;
currency: string;
name: string;
description?: string;
order_id: string;
handler: (response: RazorpayResponse) => void;
prefill?: { name?: string; email?: string; contact?: string };
theme?: { color?: string };
}
interface RazorpayResponse {
razorpay_payment_id: string;
razorpay_order_id: string;
razorpay_signature: string;
}
FAQs
4111 1111 1111 1111success@razorpayrazorpay.subscriptions.create()orders.create()Get the Code
What to Build Next
- Subscriptions for recurring SaaS billing
- Razorpay Route if you need to split payments between multiple sellers (marketplaces)
- Saved cards / tokenization for one-tap repeat purchases
- Invoice generation with text
razorpay.invoices.create()
Skip the setup and start shipping
Love this guide? All these patterns are pre-configured in our **SaaS Starter Pro** kit. Save 40+ hours of development.
Explore the KitRelated Articles
Selected insights to level up your development workflow.
How to Add Stripe to Next.js (2026)
A complete walkthrough of integrating Stripe Checkout and webhooks into your Next.js application.
Next.js + Prisma + Stripe Tutorial
Learn how to build a subscription-based SaaS using the powerhouse trio of Next.js, Prisma, and Stripe.
Next.js Authentication with Clerk — Complete Guide
Why Clerk is our top choice for SaaS authentication and how to set it up in under 15 minutes.
Keep building with free resources
Production-ready starter kits and zero-friction developer tools — the same ones we use to ship our own products.
Starter Kits
Next.js Blog Kit
MDX-powered blog with full SEO, dark mode, RSS feed, reading time, and syntax highlighting. Deploy to Vercel in one click.
Developer Tools
Shadcn/UI Component Previewer
Live preview of shadcn/ui components with instant copy-paste code. Browse rendered components and grab snippets.
Next.js Project Structure Generator
8.5kSelect your stack and instantly get a production-ready folder structure. Copy the entire scaffold in one click.
.env File Generator
24kPick your tech stack and get a complete, commented .env boilerplate file. Never forget an environment variable.
Tailwind CSS Color Palette Generator
15k+Enter a brand color and generate a complete Tailwind-compatible shade scale with config snippets.
Looking for something specific?
Browse the full library — 7+ kits across 4+ categories.