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

  1. Create an order server-side, before the client ever sees a payment button — the amount and currency should never originate from the browser
  2. Hand the order to Razorpay Checkout on the client, and treat the client-side "success" callback as informational only, never as proof of payment
  3. 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.

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.

We reply within 24 hours · No spam, ever

What happens next

  1. 1

    We reply within 24 hours

    A real person reads every message.

  2. 2

    Free 30-min strategy call

    We listen, ask questions, and tell you honestly if we can help.

  3. 3

    Written quote in 48 hours

    Detailed scope, timeline, and price. No surprises later.

Prefer WhatsApp?

Message us directly — same 24-hour reply promise, no form to fill.

+91 82876 26712