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

  1. Login to your Razorpay Dashboard
  2. Navigate to SettingsAPI Keys
  3. 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! 🚀

Complete Razorpay Integration Guide for Next.js