How to Integrate Razorpay & Cashfree in Next.js (App Router Tutorial)
Production tutorial on integrating Razorpay and Cashfree payment gateways in Next.js using App Router Route Handlers, client modals, and HMAC SHA256 webhook verification.

Razorpay and Cashfree payment gateway integration architecture in Next.js
Building online stores and SaaS checkout flows in Next.js requires secure, server-side payment orchestration. Exposing API secrets or trusting client-side callbacks leaves your application vulnerable to payment spoofing. In this tutorial, we will build a production-ready payment flow in Next.js (App Router) using Razorpay and Cashfree with serverless Route Handlers and cryptographically verified webhooks.
1. Payment Flow Architecture Overview
The secure payment lifecycle consists of four steps: 1. **Order Creation:** Client sends cart details to Next.js Route Handler (`app/api/checkout/order/route.ts`). Server creates an order via Razorpay/Cashfree API using secret keys and returns the `order_id`. 2. **Client Modal:** Client initiates the gateway JS checkout modal. 3. **Payment Execution:** User completes payment on the gateway interface. 4. **Asynchronous Verification:** The gateway triggers a webhook to your server (`app/api/webhooks/razorpay/route.ts`), which verifies the HMAC SHA256 signature and updates the order status in your database.
2. Setting Up Environment Variables
Store your API credentials safely in `.env.local`. Never prefix server secrets with `NEXT_PUBLIC_`:
# Razorpay Credentials
NEXT_PUBLIC_RAZORPAY_KEY_ID=rzp_test_your_key_id
RAZORPAY_KEY_SECRET=your_razorpay_secret_key
RAZORPAY_WEBHOOK_SECRET=your_webhook_secret_key
# Cashfree Credentials
CASHFREE_APP_ID=your_cashfree_app_id
CASHFREE_SECRET_KEY=your_cashfree_secret_key
CASHFREE_ENVIRONMENT=TEST3. Server-Side Razorpay Order Route Handler
Create `app/api/razorpay/order/route.ts` to generate orders on the server:
import { NextResponse } from 'next/server';
import Razorpay from 'razorpay';
const razorpay = new Razorpay({
key_id: process.env.NEXT_PUBLIC_RAZORPAY_KEY_ID!,
key_secret: process.env.RAZORPAY_KEY_SECRET!,
});
export async function POST(request: Request) {
try {
const { amountInRupees } = await request.json();
if (!amountInRupees || amountInRupees <= 0) {
return NextResponse.json({ error: 'Invalid payment amount' }, { status: 400 });
}
const order = await razorpay.orders.create({
amount: Math.round(amountInRupees * 100), // Amount in paise
currency: 'INR',
receipt: `rcpt_${Date.now()}`,
});
return NextResponse.json({ success: true, orderId: order.id, amount: order.amount });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}4. Verifying Webhook Signatures with HMAC SHA256
To guarantee that an order fulfillment event is genuine, verify the signature in `app/api/webhooks/razorpay/route.ts` using Node's native `crypto` module:
import { NextResponse } from 'next/server';
import crypto from 'crypto';
export async function POST(request: Request) {
try {
const rawBody = await request.text();
const signature = request.headers.get('x-razorpay-signature');
if (!signature) {
return NextResponse.json({ error: 'Missing signature' }, { status: 400 });
}
const expectedSignature = crypto
.createHmac('sha256', process.env.RAZORPAY_WEBHOOK_SECRET!)
.update(rawBody)
.digest('hex');
if (expectedSignature !== signature) {
return NextResponse.json({ error: 'Invalid webhook signature' }, { status: 400 });
}
const payload = JSON.parse(rawBody);
if (payload.event === 'payment.captured') {
const payment = payload.payload.payment.entity;
// Mark order as paid in database using payment.order_id
}
return NextResponse.json({ status: 'ok' });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}5. Testing and Common Pitfalls
Key testing practices before deploying to production: • **Paise conversion:** Razorpay amounts are measured in paise (₹100 = 10000 paise). Always calculate amounts using integer math to avoid decimal rounding errors. • **Raw body parsing:** Next.js App Router body parsing must use `request.text()` for webhook verification. Parsing JSON before signature computation will cause verification failures. • **Idempotency:** Webhook endpoints can receive duplicate events during network retries. Ensure your database updates check whether an order has already been marked as paid.
- Keep API secret keys strictly server-side inside Next.js Route Handlers.
- Use integer amounts in the smallest currency unit (paise) to prevent float issues.
- Fulfill orders only after validating raw body HMAC SHA256 webhook signatures.
- Handle webhook idempotency to prevent duplicate fulfillment during gateway retries.

