Complete Razorpay Integration Guide for Next.js
Razorpay is one of India's most popular payment gateways, offering seamless integration for web applications. In this comprehensive guide, we'll walk through integrating Razorpay into your Next.js application with TypeScript, proper error handling, and best practices.
What You'll Learn
- Setting up Razorpay API keys securely
- Creating and verifying payment orders
- Building reusable payment components
- Implementing proper error handling
- TypeScript integration and type safety
- Security best practices
Prerequisites
Before we start, ensure you have:
- A Next.js 14+ application with TypeScript
- A Razorpay account (Sign up here)
- Basic knowledge of React and TypeScript
Step 1: Install Required Packages
Install the Razorpay Node.js SDK and other necessary packages:
npm install razorpay @types/razorpay axios
Step 2: Environment Setup
Get Your API Keys
- Login to your Razorpay Dashboard
- Navigate to Settings → API Keys
- Generate your API keys (use test keys for development)
Configure Environment Variables
Create a .env.local
file in your project root:
# Razorpay Configuration
NEXT_PUBLIC_RAZORPAY_KEY_ID=rzp_test_xxxxxxxxxx
RAZORPAY_KEY_ID=rzp_test_xxxxxxxxxx
RAZORPAY_KEY_SECRET=your_secret_key_here
# App Configuration
NEXT_PUBLIC_APP_URL=http://localhost:3000
Security Note: Never expose your secret key in client-side code. Only use
NEXT_PUBLIC_
for the key ID.
Step 3: Type Definitions
Create types/razorpay.ts
for type safety:
export interface CreateOrderRequest {
amount: number;
currency: string;
receipt?: string;
notes?: Record<string, string>;
}
export interface CreateOrderResponse {
success: boolean;
order?: {
id: string;
amount: number;
currency: string;
receipt: string;
status: string;
};
error?: string;
}
export interface PaymentVerificationRequest {
razorpay_order_id: string;
razorpay_payment_id: string;
razorpay_signature: string;
}
export interface PaymentVerificationResponse {
success: boolean;
message?: string;
error?: string;
}
export interface RazorpayInstance {
open: () => void;
on: (event: string, handler: (response: any) => void) => void;
}
export interface RazorpayResponse {
razorpay_order_id: string;
razorpay_payment_id: string;
razorpay_signature: string;
}
export interface 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;
};
}
declare global {
interface Window {
Razorpay: new (options: RazorpayOptions) => RazorpayInstance;
}
}
Step 4: API Routes
Create Order API
Create app/api/payment/create-order/route.ts
:
import { NextRequest, NextResponse } from "next/server";
import Razorpay from "razorpay";
import { CreateOrderRequest, CreateOrderResponse } from "@/types/razorpay";
const razorpay = new Razorpay({
key_id: process.env.RAZORPAY_KEY_ID!,
key_secret: process.env.RAZORPAY_KEY_SECRET!,
});
export async function POST(request: NextRequest) {
try {
const body: CreateOrderRequest = await request.json();
// Validate request
if (!body.amount || body.amount <= 0) {
return NextResponse.json<CreateOrderResponse>(
{ success: false, error: "Valid amount is required" },
{ status: 400 }
);
}
if (!body.currency) {
return NextResponse.json<CreateOrderResponse>(
{ success: false, error: "Currency is required" },
{ status: 400 }
);
}
const options = {
amount: body.amount * 100, // Convert to paise
currency: body.currency,
receipt: body.receipt || `receipt_${Date.now()}`,
notes: body.notes || {},
};
const order = await razorpay.orders.create(options);
return NextResponse.json<CreateOrderResponse>({
success: true,
order: {
id: order.id,
amount: order.amount,
currency: order.currency,
receipt: order.receipt,
status: order.status,
},
});
} catch (error) {
console.error("Order creation error:", error);
return NextResponse.json<CreateOrderResponse>(
{ success: false, error: "Failed to create order" },
{ status: 500 }
);
}
}
Payment Verification API
Create app/api/payment/verify/route.ts
:
import { NextRequest, NextResponse } from "next/server";
import crypto from "crypto";
import { PaymentVerificationRequest, PaymentVerificationResponse } from "@/types/razorpay";
export async function POST(request: NextRequest) {
try {
const body: PaymentVerificationRequest = await request.json();
const { razorpay_order_id, razorpay_payment_id, razorpay_signature } = body;
// Validate required fields
if (!razorpay_order_id || !razorpay_payment_id || !razorpay_signature) {
return NextResponse.json<PaymentVerificationResponse>(
{ success: false, error: "Missing required payment parameters" },
{ status: 400 }
);
}
const secret = process.env.RAZORPAY_KEY_SECRET!;
// Generate expected signature
const sign = razorpay_order_id + "|" + razorpay_payment_id;
const expectedSign = crypto
.createHmac("sha256", secret)
.update(sign)
.digest("hex");
if (razorpay_signature === expectedSign) {
// Payment verified successfully
// TODO: Update your database, send emails, etc.
return NextResponse.json<PaymentVerificationResponse>({
success: true,
message: "Payment verified successfully",
});
} else {
return NextResponse.json<PaymentVerificationResponse>(
{ success: false, error: "Invalid payment signature" },
{ status: 400 }
);
}
} catch (error) {
console.error("Payment verification error:", error);
return NextResponse.json<PaymentVerificationResponse>(
{ success: false, error: "Payment verification failed" },
{ status: 500 }
);
}
}
Step 5: Payment Utilities
Create lib/payment.ts
:
import axios from "axios";
import { CreateOrderRequest, CreateOrderResponse, PaymentVerificationRequest, PaymentVerificationResponse } from "@/types/razorpay";
export class PaymentService {
static async createOrder(orderData: CreateOrderRequest): Promise<CreateOrderResponse> {
try {
const response = await axios.post<CreateOrderResponse>(
"/api/payment/create-order",
orderData
);
return response.data;
} catch (error) {
console.error("Failed to create order:", error);
throw new Error("Failed to create payment order");
}
}
static async verifyPayment(paymentData: PaymentVerificationRequest): Promise<PaymentVerificationResponse> {
try {
const response = await axios.post<PaymentVerificationResponse>(
"/api/payment/verify",
paymentData
);
return response.data;
} catch (error) {
console.error("Failed to verify payment:", error);
throw new Error("Payment verification failed");
}
}
}
Step 6: Payment Hook
Create hooks/usePayment.ts
:
import { useState, useCallback } from "react";
import { PaymentService } from "@/lib/payment";
import { RazorpayResponse } from "@/types/razorpay";
interface PaymentOptions {
amount: number;
currency?: string;
productName: string;
description: string;
userDetails?: {
name?: string;
email?: string;
contact?: string;
};
}
interface PaymentState {
isLoading: boolean;
error: string | null;
success: boolean;
}
export const usePayment = () => {
const [state, setState] = useState<PaymentState>({
isLoading: false,
error: null,
success: false,
});
const processPayment = useCallback(async (
options: PaymentOptions,
onSuccess?: (response: RazorpayResponse) => void,
onError?: (error: string) => void
) => {
setState({ isLoading: true, error: null, success: false });
try {
// Create order
const orderResponse = await PaymentService.createOrder({
amount: options.amount,
currency: options.currency || "INR",
notes: {
productName: options.productName,
description: options.description,
},
});
if (!orderResponse.success || !orderResponse.order) {
throw new Error(orderResponse.error || "Failed to create order");
}
const { order } = orderResponse;
// Configure Razorpay options
const razorpayOptions = {
key: process.env.NEXT_PUBLIC_RAZORPAY_KEY_ID!,
amount: order.amount,
currency: order.currency,
name: options.productName,
description: options.description,
order_id: order.id,
handler: async (response: RazorpayResponse) => {
try {
const verificationResponse = await PaymentService.verifyPayment(response);
if (verificationResponse.success) {
setState({ isLoading: false, error: null, success: true });
onSuccess?.(response);
} else {
throw new Error(verificationResponse.error || "Payment verification failed");
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Payment verification failed";
setState({ isLoading: false, error: errorMessage, success: false });
onError?.(errorMessage);
}
},
prefill: options.userDetails || {},
theme: {
color: "#3B82F6",
},
};
// Open Razorpay checkout
const razorpay = new window.Razorpay(razorpayOptions);
razorpay.on("payment.failed", (response: any) => {
const errorMessage = response.error?.description || "Payment failed";
setState({ isLoading: false, error: errorMessage, success: false });
onError?.(errorMessage);
});
razorpay.open();
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Payment failed";
setState({ isLoading: false, error: errorMessage, success: false });
onError?.(errorMessage);
}
}, []);
const resetState = useCallback(() => {
setState({ isLoading: false, error: null, success: false });
}, []);
return {
...state,
processPayment,
resetState,
};
};
Step 7: Payment Component
Create components/PaymentButton.tsx
:
"use client";
import React, { useEffect } from "react";
import Script from "next/script";
import { usePayment } from "@/hooks/usePayment";
import { RazorpayResponse } from "@/types/razorpay";
interface PaymentButtonProps {
amount: number;
currency?: string;
productName: string;
description: string;
userDetails?: {
name?: string;
email?: string;
contact?: string;
};
onSuccess?: (response: RazorpayResponse) => void;
onError?: (error: string) => void;
className?: string;
disabled?: boolean;
}
export default function PaymentButton({
amount,
currency = "INR",
productName,
description,
userDetails,
onSuccess,
onError,
className = "",
disabled = false,
}: PaymentButtonProps) {
const { isLoading, error, success, processPayment, resetState } = usePayment();
useEffect(() => {
if (error) {
onError?.(error);
}
}, [error, onError]);
const handlePayment = () => {
if (disabled || isLoading) return;
processPayment(
{
amount,
currency,
productName,
description,
userDetails,
},
onSuccess,
onError
);
};
const baseClassName = `
px-6 py-3 rounded-lg font-semibold text-white
bg-blue-600 hover:bg-blue-700
disabled:bg-gray-400 disabled:cursor-not-allowed
transition-all duration-200
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
`;
return (
<>
<button
onClick={handlePayment}
disabled={disabled || isLoading}
className={`${baseClassName} ${className}`}
>
{isLoading ? (
<span className="flex items-center gap-2">
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
Processing...
</span>
) : (
`Pay ₹${amount}`
)}
</button>
<Script
id="razorpay-checkout-js"
src="https://checkout.razorpay.com/v1/checkout.js"
strategy="lazyOnload"
/>
</>
);
}
Step 8: Usage Example
Create a product page using the PaymentButton:
"use client";
import { useState } from "react";
import PaymentButton from "@/components/PaymentButton";
import { RazorpayResponse } from "@/types/razorpay";
export default function ProductPage() {
const [paymentStatus, setPaymentStatus] = useState<string>("");
const handlePaymentSuccess = (response: RazorpayResponse) => {
setPaymentStatus("Payment successful! Order ID: " + response.razorpay_order_id);
// Redirect to success page or update UI
};
const handlePaymentError = (error: string) => {
setPaymentStatus("Payment failed: " + error);
};
return (
<div className="max-w-2xl mx-auto p-8">
<div className="bg-white rounded-lg shadow-lg p-6">
<h1 className="text-3xl font-bold mb-4 text-gray-800">
Premium Course Access
</h1>
<div className="mb-6">
<img
src="/course-preview.jpg"
alt="Course Preview"
className="w-full h-64 object-cover rounded-lg"
/>
</div>
<div className="mb-6">
<p className="text-gray-600 mb-4">
Get lifetime access to our premium web development course including:
</p>
<ul className="list-disc list-inside space-y-2 text-gray-600">
<li>50+ hours of video content</li>
<li>Project-based learning</li>
<li>Certificate of completion</li>
<li>1-year support</li>
</ul>
</div>
<div className="flex items-center justify-between mb-6">
<div>
<span className="text-3xl font-bold text-gray-800">₹2,999</span>
<span className="text-lg text-gray-500 line-through ml-2">₹4,999</span>
</div>
<span className="bg-green-100 text-green-800 px-3 py-1 rounded-full text-sm font-medium">
40% OFF
</span>
</div>
<PaymentButton
amount={2999}
currency="INR"
productName="Premium Web Development Course"
description="Lifetime access to premium course content"
userDetails={{
name: "John Doe",
email: "john@example.com",
contact: "+91 9876543210",
}}
onSuccess={handlePaymentSuccess}
onError={handlePaymentError}
className="w-full"
/>
{paymentStatus && (
<div className={`mt-4 p-4 rounded-lg ${
paymentStatus.includes("successful")
? "bg-green-100 text-green-800"
: "bg-red-100 text-red-800"
}`}>
{paymentStatus}
</div>
)}
</div>
</div>
);
}
Step 9: Error Handling & Loading States
Create a comprehensive error handling component:
"use client";
import { AlertCircle, CheckCircle, Loader2 } from "lucide-react";
interface PaymentStatusProps {
status: "idle" | "loading" | "success" | "error";
message?: string;
}
export function PaymentStatus({ status, message }: PaymentStatusProps) {
if (status === "idle") return null;
const configs = {
loading: {
icon: <Loader2 className="w-5 h-5 animate-spin" />,
className: "bg-blue-50 text-blue-700 border-blue-200",
title: "Processing Payment",
},
success: {
icon: <CheckCircle className="w-5 h-5" />,
className: "bg-green-50 text-green-700 border-green-200",
title: "Payment Successful",
},
error: {
icon: <AlertCircle className="w-5 h-5" />,
className: "bg-red-50 text-red-700 border-red-200",
title: "Payment Failed",
},
};
const config = configs[status];
return (
<div className={`p-4 rounded-lg border ${config.className}`}>
<div className="flex items-center gap-3">
{config.icon}
<div>
<h3 className="font-semibold">{config.title}</h3>
{message && <p className="text-sm mt-1">{message}</p>}
</div>
</div>
</div>
);
}
Security Best Practices
1. Environment Variables
- Never expose secret keys in client-side code
- Use different keys for development and production
- Regularly rotate your API keys
2. Payment Verification
- Always verify payments on the server side
- Use webhook notifications for critical updates
- Implement idempotency for payment processing
3. Error Handling
- Implement proper error boundaries
- Log payment failures for debugging
- Provide clear user feedback
4. Data Validation
- Validate all payment data on both client and server
- Sanitize user inputs
- Implement rate limiting for payment APIs
Testing
Test Mode
Use Razorpay's test mode for development:
// Test card numbers
const testCards = {
success: "4111111111111111",
failure: "4000000000000002",
insufficient: "4000000000000101",
};
Testing Checklist
- [ ] Successful payment flow
- [ ] Payment failure scenarios
- [ ] Network error handling
- [ ] Invalid signature handling
- [ ] Webhook processing
- [ ] Mobile responsiveness
Deployment Considerations
Environment Setup
# Production environment variables
NEXT_PUBLIC_RAZORPAY_KEY_ID=rzp_live_xxxxxxxxxx
RAZORPAY_KEY_ID=rzp_live_xxxxxxxxxx
RAZORPAY_KEY_SECRET=your_live_secret_key
# Additional production configs
NEXT_PUBLIC_APP_URL=https://yourdomain.com
WEBHOOK_SECRET=your_webhook_secret
Security Headers
Add security headers in next.config.js
:
module.exports = {
async headers() {
return [
{
source: '/api/payment/:path*',
headers: [
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'X-Frame-Options',
value: 'DENY',
},
],
},
];
},
};
Conclusion
You've successfully implemented a complete Razorpay integration with:
✅ Type Safety: Full TypeScript support
✅ Security: Server-side verification and secure key management
✅ Error Handling: Comprehensive error handling and user feedback
✅ Reusability: Modular components and hooks
✅ Best Practices: Following industry standards
✅ Testing: Test mode integration and validation
Next Steps
- Implement webhook handling for real-time updates
- Add support for subscription payments
- Integrate with your database for order management
- Add analytics and payment tracking
- Implement refund functionality
Resources
Happy coding! 🚀