01 What's Included
Razorpay Utility Library
Create orders, verify payments, validate webhooks β all typed and tested.
API Routes (3)
Order creation, payment verification, webhook handler β production-ready Next.js route handlers.
Payment Button Component
Drop-in React component with loading states, error handling, and success callbacks.
TypeScript Types
Full type definitions for Razorpay options, responses, orders, and webhooks.
Tech Stack
- Next.js 14+ (App Router, Route Handlers)
- TypeScript (strict mode compatible)
- Razorpay SDK (razorpay + razorpay-react)
- INR / UPI / Cards / Netbanking β all payment methods supported
Prerequisites
- Node.js 18+ and npm/pnpm
- Next.js 14+ project with App Router
- Razorpay account (free to create, pay only transaction fees)
- Basic understanding of API routes and React state
02 Setup Guide
Step 1 β Create a Razorpay Account
- Go to razorpay.com and click "Sign Up"
- Enter your business email, set a password
- Complete KYC: PAN, Aadhaar, bank account details
- Account activates in 2β3 business days (test mode works instantly)
Step 2 β Get API Keys
- Log into Razorpay Dashboard β Settings β API Keys
- Click "Generate Key Pair"
- Copy the Key ID (starts with
rzp_test_) and Key Secret - For live: repeat with Key ID starting
rzp_live_
Step 3 β Install Dependencies
# Using npm
npm install razorpay
# Using pnpm
pnpm add razorpay
# Using yarn
yarn add razorpay
Step 4 β Environment Variables
Copy the .env.example file from the code section below to .env.local in your project root and fill in your keys.
# Razorpay
RAZORPAY_KEY_ID=rzp_test_xxxxxxxxxxxxx
RAZORPAY_KEY_SECRET=xxxxxxxxxxxxxxxxxxxxxxxx
RAZORPAY_WEBHOOK_SECRET=your_webhook_secret_here
03 Code Files
a) lib/razorpay.ts β Razorpay Utility
import Razorpay from "razorpay";
import crypto from "crypto";
const razorpay = new Razorpay({
key_id: process.env.RAZORPAY_KEY_ID!,
key_secret: process.env.RAZORPAY_KEY_SECRET!,
});
interface CreateOrderParams {
amount: number; // in INR (e.g., 999)
currency?: string;
receipt?: string;
notes?: Record<string, string>;
}
export async function createOrder(params: CreateOrderParams) {
const order = await razorpay.orders.create({
amount: Math.round(params.amount * 100), // paisa
currency: params.currency ?? "INR",
receipt: params.receipt,
notes: params.notes,
});
return order;
}
export function verifyPayment(
orderId: string,
paymentId: string,
signature: string
): boolean {
const body = orderId + "|" + paymentId;
const expected = crypto
.createHmac("sha256", process.env.RAZORPAY_KEY_SECRET!)
.update(body)
.digest("hex");
return expected === signature;
}
export function verifyWebhook(
body: string,
signature: string
): boolean {
const expected = crypto
.createHmac("sha256", process.env.RAZORPAY_WEBHOOK_SECRET!)
.update(body)
.digest("hex");
return expected === signature;
}
export default razorpay;
b) app/api/razorpay/order/route.ts β Create Order
import { NextResponse } from "next/server";
import { createOrder } from "@/lib/razorpay";
export async function POST(req: Request) {
try {
const { amount, receipt, notes } = await req.json();
if (!amount || typeof amount !== "number" || amount < 1) {
return NextResponse.json(
{ error: "Invalid amount" },
{ status: 400 }
);
}
const order = await createOrder({
amount,
receipt ?? `rcpt_${Date.now()}`,
notes: { ...notes, source: "nextjs-app" },
});
return NextResponse.json({
orderId: order.id,
amount: order.amount,
currency: order.currency,
});
} catch (err) {
console.error("Order creation failed:", err);
return NextResponse.json(
{ error: "Failed to create order" },
{ status: 500 }
);
}
}
c) app/api/razorpay/verify/route.ts β Verify Payment
import { NextResponse } from "next/server";
import { verifyPayment } from "@/lib/razorpay";
export async function POST(req: Request) {
try {
const { razorpay_order_id, razorpay_payment_id, razorpay_signature } =
await req.json();
if (!razorpay_order_id || !razorpay_payment_id || !razorpay_signature) {
return NextResponse.json(
{ error: "Missing payment parameters" },
{ status: 400 }
);
}
const isValid = verifyPayment(
razorpay_order_id,
razorpay_payment_id,
razorpay_signature
);
if (!isValid) {
return NextResponse.json(
{ error: "Invalid signature" },
{ status: 400 }
);
}
// TODO: Update your database
// await db.orders.update({
// where: { razorpayOrderId: razorpay_order_id },
// data: {
// status: "paid",
// razorpayPaymentId: razorpay_payment_id,
// },
// });
return NextResponse.json({ success: true });
} catch (err) {
console.error("Payment verification failed:", err);
return NextResponse.json(
{ error: "Verification failed" },
{ status: 500 }
);
}
}
d) app/api/razorpay/webhook/route.ts β Webhook Handler
import { NextResponse } from "next/server";
import { verifyWebhook } from "@/lib/razorpay";
export async function POST(req: Request) {
const body = await req.text();
const signature = req.headers.get("x-razorpay-signature") ?? "";
if (!verifyWebhook(body, signature)) {
return NextResponse.json(
{ error: "Invalid webhook signature" },
{ status: 400 }
);
}
const event = JSON.parse(body);
switch (event.event) {
case "payment.captured": {
const payment = event.payload.payment.entity;
// TODO: Fulfill order β send email, grant access, etc.
console.log("Payment captured:", payment.id);
break;
}
case "payment.failed": {
const failed = event.payload.payment.entity;
console.log("Payment failed:", failed.id);
// TODO: Notify user, update order status
break;
}
case "refund.created": {
const refund = event.payload.refund.entity;
console.log("Refund created:", refund.id);
// TODO: Update order, notify user
break;
}
default:
console.log("Unhandled event:", event.event);
}
return NextResponse.json({ received: true });
}
https://yourdomain.com/api/razorpay/webhook. Select events: payment.captured, payment.failed, refund.created.e) components/PaymentButton.tsx β Payment Component
"use client";
import { useState } from "react";
import type { RazorpayOptions, RazorpayResponse } from "@/types/razorpay";
interface PaymentButtonProps {
amount: number;
name: string;
description?: string;
onSuccess?: (response: RazorpayResponse) => void | Promise<void>;
onError?: (error: string) => void;
}
export default function PaymentButton({
amount,
name,
description,
onSuccess,
onError,
}: PaymentButtonProps) {
const [loading, setLoading] = useState(false);
const handlePayment = async () => {
setLoading(true);
try {
// 1. Create order
const res = await fetch("/api/razorpay/order", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ amount, receipt: `rcpt_${Date.now()}` }),
});
const { orderId } = await res.json();
if (!orderId) throw new Error("Failed to create order");
// 2. Open Razorpay checkout
const options: RazorpayOptions = {
key: process.env.NEXT_PUBLIC_RAZORPAY_KEY_ID!,
amount: String(amount * 100),
currency: "INR",
name,
description: description ?? name,
order_id: orderId,
handler: async (response: RazorpayResponse) => {
// 3. Verify payment
const verify = await fetch("/api/razorpay/verify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(response),
});
const { success } = await verify.json();
if (success) {
onSuccess?.(response);
} else {
throw new Error("Payment verification failed");
}
},
prefill: { method: "upi" },
theme: { color: "#aa0404" },
};
const rzp = new window.Razorpay(options);
rzp.on("payment.failed", (resp: any) => {
const msg = resp.error?.description ?? "Payment failed";
onError?.(msg);
});
rzp.open();
} catch (err) {
onError?.((err as Error).message ?? "Something went wrong");
} finally {
setLoading(false);
}
};
return (
<button
onClick={handlePayment}
disabled={loading}
style={{
background: loading ? "#555" : "#aa0404",
color: "#fff",
padding: "12px 32px",
border: "none",
borderRadius: "8px",
fontSize: "1rem",
cursor: loading ? "not-allowed" : "pointer",
fontWeight: 600,
}}
>
{loading ? "Processingβ¦" : `Pay βΉ${amount}`}
</button>
);
}
f) types/razorpay.d.ts β TypeScript Types
export interface RazorpayOptions {
key: string;
amount: string;
currency: string;
name: string;
description?: string;
order_id: string;
handler: (response: RazorpayResponse) => void;
prefill?: {
name?: string;
email?: string;
contact?: string;
method?: "upi" | "card" | "netbanking" | "wallet";
};
theme?: { color?: string };
modal?: { ondismiss?: () => void };
notes?: Record<string, string>;
}
export interface RazorpayResponse {
razorpay_order_id: string;
razorpay_payment_id: string;
razorpay_signature: string;
}
export interface OrderDetails {
id: string;
amount: number;
currency: string;
status: "created" | "attempted" | "paid";
receipt?: string;
notes?: Record<string, string>;
}
// Extend Window for Razorpay checkout
declare global {
interface Window {
Razorpay: new (options: RazorpayOptions) => {
open: () => void;
on: (event: string, cb: Function) => void;
};
}
}
g) .env.example β Environment Template
# Razorpay β Server-side (NEVER expose to client)
RAZORPAY_KEY_ID=rzp_test_xxxxxxxxxxxxx
RAZORPAY_KEY_SECRET=xxxxxxxxxxxxxxxxxxxxxxxx
RAZORPAY_WEBHOOK_SECRET=your_webhook_secret_here
# Razorpay β Client-side (safe for browser)
NEXT_PUBLIC_RAZORPAY_KEY_ID=rzp_test_xxxxxxxxxxxxx
X How to Use This Product
- Install dependencies:
npm install razorpayin your Next.js 14+ project. - Copy files in order: Types β Library β API Routes β Webhook β Frontend.
- Set up environment variables with your Razorpay test keys from the dashboard.
- Test with Razorpay test mode (rzp_test_...) during development.
- Register your webhook URL in Razorpay Dashboard β Settings β Webhooks.
X Use Cases
SaaS subscriptions
Set up recurring payments for monthly/annual subscription products.
Digital product sales
One-time payment integration for selling ebooks, courses, or templates.
E-commerce checkout
Complete payment flow with Razorpay for online stores.
Marketplace platforms
Handle payments from multiple sellers with split payments.
Event ticketing
Accept payments for workshops, webinars, or conferences.
X How This Helps You
- Saves weeks of development β complete, production-ready code instead of scattered documentation.
- TypeScript-first β full type safety catches errors before they reach production.
- India-optimized β handles INR directly, Indian payment methods (UPI, NetBanking, Wallets).
- Webhook handling included β payment status updates without polling.
- Tested patterns β follows Razorpay's official recommendations with real-world additions.
04 Integration Guide
Product Page β One-time Payment
Use PaymentButton directly. Pass amount in INR (no paise conversion needed β the component handles it).
import PaymentButton from "@/components/PaymentButton";
export default function ProductPage() {
return (
<div>
<h1>My Awesome Product</h1>
<p>βΉ999 β Lifetime access</p>
<PaymentButton
amount={999}
name="My Awesome Product"
onSuccess={(res) => {
window.location.href = `/success?order=${res.razorpay_order_id}`;
}}
/>
</div>
);
}
Course / Subscription Payment
Pass additional notes for tracking. Use the webhook to grant access after confirmation.
"use client";
import { useState } from "react";
import PaymentButton from "@/components/PaymentButton";
export default function CourseEnroll({ courseId, price }: { courseId: string; price: number }) {
const [status, setStatus] = useState<"idle" | "success">("idle");
if (status === "success") {
return <p>π Enrollment confirmed! Check your email.</p>;
}
return (
<PaymentButton
amount={price}
name="Course Enrollment"
onSuccess={async () => {
// Webhook handles actual enrollment
setStatus("success");
}}
onError={(msg) => alert(msg)}
/>
);
}
Refund Handling
Process refunds via Razorpay Dashboard or API. The webhook (refund.created) auto-catches it.
import razorpay from "@/lib/razorpay";
// Partial refund
await razorpay.payments.refund("pay_xxxxx", {
amount: 50000, // βΉ500 in paisa
notes: { reason: "Customer request" },
});
// Full refund
await razorpay.payments.refund("pay_xxxxx");
05 Common Issues & Fixes
| Issue | Cause | Fix |
|---|---|---|
| CORS error on checkout | Razorpay script not loaded | Add <script src="https://checkout.razorpay.com/v1/checkout.js"> in layout.tsx or load it dynamically before checkout |
| Webhook not receiving events | Wrong URL or not publicly accessible | Use ngrok for local testing. Ensure URL ends with /api/razorpay/webhook. Check webhook secret matches env var. |
| Payment captured but order not updated | Webhook didn't fire or handler threw | Check Razorpay Dashboard β Webhooks β Logs. Add error logging in webhook handler. Re-trigger from dashboard. |
| Test vs live key confusion | Using live key in dev or test key in prod | Use NODE_ENV to switch, or separate env files. Test keys = rzp_test_, live = rzp_live_ |
| "Unauthorized" on order creation | Missing or wrong key secret | Double-check RAZORPAY_KEY_SECRET in .env.local. No trailing spaces. |
| Signature mismatch on verify | Secret mismatch or body tampered | Ensure RAZORPAY_KEY_SECRET is identical to dashboard. Verify body isn't modified between receipt and verification. |
06 Checklists
Pre-Launch Checklist
- All API routes return proper error responses (not 500 stack traces)
- Payment verification runs server-side (never trust client alone)
- Webhook signature verification is active
- Amount validation prevents zero/negative/overflow amounts
- Environment variables are in
.env.local, not committed to git - Razorpay script loaded before checkout opens
- Test with all payment methods: UPI, cards, netbanking
- Test failed payment flow (use test card
4111 1111 1111 1111) - Test payment dismissed by user (modal closed without paying)
- Idempotency: duplicate webhook calls don't double-charge or double-fulfill
Go-Live Checklist
- Switch from test to live keys in environment variables
- Update webhook URL in Razorpay Dashboard to production domain
- Verify webhook secret matches production env var
- Enable live payment methods in Dashboard (UPI, Cards, Netbanking, Wallets)
- Test one real βΉ1 transaction end-to-end
- Verify email notifications fire on payment.captured
- Check Razorpay Dashboard for successful webhook delivery
- Set up Razorpay payout schedule for your bank account
- Monitor first 24 hours for failed webhooks or signature errors