Razorpay Integration for Next.js E-commerce: A Developer's Guide
By EcomWeb Team · 22 Jul 2026 · 9 min read
Most Razorpay integration guides stop at "call Checkout.js and you're done." That covers maybe half of what a production integration actually needs. This walks through the full shape of the flow on a Next.js storefront: server-side order creation, the client Checkout handoff, and — the part most tutorials skip — verifying webhook signatures so your order-fulfillment logic isn't trusting the client.
The three pieces of a real integration
- Create an order server-side, before the client ever sees a payment button — the amount and currency should never originate from the browser
- Hand the order to Razorpay Checkout on the client, and treat the client-side "success" callback as informational only, never as proof of payment
- Verify payment via a webhook, whose signature you check server-side before marking an order paid — this is the actual source of truth
1. Creating the order server-side
In a Next.js App Router project, this is a natural fit for a Server Action or a Route Handler — the same pattern you'd use for any server-only logic that needs a secret key. The amount has to be in the smallest currency unit (paise for INR), which is the single most common bug in a first integration: sending 499 instead of 49900 either fails outright or, worse, charges ₹4.99 instead of ₹499.
// app/actions/create-order.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!, // server-only, never exposed to the client
});
export async function createOrder(amountInRupees: number, receiptId: string) {
const order = await razorpay.orders.create({
amount: Math.round(amountInRupees * 100), // paise, not rupees
currency: 'INR',
receipt: receiptId,
});
return { orderId: order.id, amount: order.amount };
}2. Handing off to Checkout on the client
The client only ever sees the public key_id and the order_id returned from the server action above — never the secret key. Checkout.js opens Razorpay's hosted payment UI and returns a payment_id, order_id, and signature to the success handler. That handler is a convenience for UX (showing a confirmation state) — it is not where you should mark an order as paid.
const options = {
key: process.env.NEXT_PUBLIC_RAZORPAY_KEY_ID,
amount,
currency: 'INR',
order_id: orderId,
handler: function () {
// UX only — show a "processing" state.
// Do NOT mark the order paid here.
},
};
new window.Razorpay(options).open();3. Verifying the webhook — the part that actually matters
A client-side success callback can be spoofed, skipped, or simply never fire if the browser tab closes at the wrong moment. Razorpay's webhook is the authoritative signal: configure a webhook URL in the Razorpay dashboard, and verify its signature server-side before touching your order state. The signature is an HMAC SHA-256 of the raw request body, keyed with a webhook secret you set separately from your API keys.
// app/api/webhooks/razorpay/route.ts
import crypto from 'crypto';
export async function POST(req: Request) {
const rawBody = await req.text();
const signature = req.headers.get('x-razorpay-signature') ?? '';
const expected = crypto
.createHmac('sha256', process.env.RAZORPAY_WEBHOOK_SECRET!)
.update(rawBody)
.digest('hex');
if (expected !== signature) {
return new Response('Invalid signature', { status: 400 });
}
const event = JSON.parse(rawBody);
if (event.event === 'payment.captured') {
// Now it's safe to mark the order paid.
}
return new Response('ok', { status: 200 });
}Pitfalls that actually bite in production
- Reading the request body as JSON before computing the signature — the HMAC has to run against the exact raw bytes Razorpay sent, not a re-serialized object, or the signature will never match
- Not handling webhook retries idempotently — Razorpay will retry a webhook that doesn't return a 2xx response, so marking an already-paid order paid again should be a safe no-op, not a duplicate fulfillment
- Mixing up test-mode and live-mode keys — a test key pair against a live webhook secret (or vice versa) fails silently in ways that look like a broken integration rather than a config mismatch
- Trusting the amount from the client on the success callback instead of re-checking it against what the server-side order actually specified
Where this fits in a custom build
This is the exact pattern every custom e-commerce build we ship uses for Razorpay — order creation as a server-only action, Checkout as a thin client handoff, and webhook verification as the real source of truth for order state. It generalizes to Stripe with the equivalent signing-secret verification step, which is why multi-gateway support (Razorpay + Stripe) doesn't mean duplicating the trust model, just the client SDK.
More from the blog
18 Jun 2026 · 7 min read
How Much Does a Custom Website Cost in India? (2026 Pricing Guide)
A transparent breakdown of what a hand-coded custom website actually costs in India in 2026, tier by tier — and why most agencies won't give you a straight number.
2 Jul 2026 · 8 min read
Custom E-commerce vs. Shopify: Which Is Right for Your Indian Business?
A practical comparison of custom-coded e-commerce and Shopify for Indian businesses — real costs, real trade-offs, and how to know which one fits where you are today.
Start a project
Let's build something worth shipping.
Tell us about your custom website or AI e-commerce project. Free 30-minute strategy call in India before any commitment.
What happens next
- 1
We reply within 24 hours
A real person reads every message.
- 2
Free 30-min strategy call
We listen, ask questions, and tell you honestly if we can help.
- 3
Written quote in 48 hours
Detailed scope, timeline, and price. No surprises later.