# Razorpay + Next.js Server Actions: The Payment Flow I Use in Production
By Govind Bajaj · 2025

Tags: razorpay, payments, next.js, server-actions, ecommerce
I processed 2,400 payments through Razorpay last month. Zero fraud cases. 99.2% success rate. Here's the exact Server Action flow I use for Sans Herbals.
I processed 2,400 payments through Razorpay last month on the Sans Herbals store.

Zero fraud cases. 99.2% success rate. The implementation uses Next.js Server Actions for the entire flow — order creation, payment verification, and webhook handling. No API routes. No client-side secret exposure. Clean, type-safe, and production-hardened.

## Why Server Actions for Payments

Server Actions run exclusively on the server. Your Razorpay secret key never touches the client. The payment flow is a single function call from the frontend, with all sensitive operations server-side.

Compare this to the traditional REST API approach: client calls /api/create-order, which calls Razorpay, which returns an order ID, which the client uses to open the checkout. Three network hops, two of which expose payment state to the client.

With Server Actions: client calls a function, server creates the order and returns the checkout config, client opens Razorpay. Two hops, one of which is a direct function call that feels instant.

## The Payment Action

```ts
// app/actions/payment.ts
'use server'

import Razorpay from 'razorpay'
import { redirect } from 'next/navigation'

const razorpay = new Razorpay({
  key_id: process.env.RAZORPAY_KEY_ID!,
  key_secret: process.env.RAZORPAY_KEY_SECRET!,
})

export async function createOrder(items: CartItem[]) {
  // Calculate total
  const amount = items.reduce((sum, item) => 
    sum + item.price * item.quantity * 100, 0 // paise
  )

  const order = await razorpay.orders.create({
    amount,
    currency: 'INR',
    receipt: `order_${Date.now()}`,
    notes: {
      itemCount: items.length.toString(),
    },
  })

  return {
    orderId: order.id,
    amount: order.amount,
    currency: order.currency,
    keyId: process.env.RAZORPAY_KEY_ID!,
  }
}

export async function verifyPayment(
  orderId: string,
  paymentId: string,
  signature: string
) {
  const crypto = await import('crypto')
  
  const body = orderId + '|' + paymentId
  const expected = crypto
    .createHmac('sha256', process.env.RAZORPAY_KEY_SECRET!)
    .update(body)
    .digest('hex')

  if (expected !== signature) {
    throw new Error('Invalid payment signature')
  }

  // Update order status in database
  await updateOrderStatus(orderId, 'paid', paymentId)
  
  redirect('/order/success?payment=' + paymentId)
}
```

## The Checkout Component

```tsx
// components/CheckoutButton.tsx
'use client'

import { createOrder, verifyPayment } from '@/app/actions/payment'

export function CheckoutButton({ items }: { items: CartItem[] }) {
  async function handleCheckout() {
    const { orderId, amount, keyId } = await createOrder(items)

    const options = {
      key: keyId,
      amount,
      order_id: orderId,
      name: 'Sans Herbals',
      description: 'Order Payment',
      handler: async (response: any) => {
        await verifyPayment(
          response.razorpay_order_id,
          response.razorpay_payment_id,
          response.razorpay_signature
        )
      },
      prefill: {
        email: 'customer@example.com',
      },
      theme: { color: '#059669' },
    }

    const razorpay = new (window as any).Razorpay(options)
    razorpay.open()
  }

  return (
    <button onClick={handleCheckout}>
      Pay with Razorpay
    </button>
  )
}
```

## Webhook Handling

```ts
// app/api/webhooks/razorpay/route.ts
import { NextRequest } from 'next/server'
import { validateWebhookSignature } from 'razorpay/dist/utils/razorpay-utils'

export async function POST(req: NextRequest) {
  const body = await req.text()
  const signature = req.headers.get('x-razorpay-signature')

  const isValid = validateWebhookSignature(
    body,
    signature!,
    process.env.RAZORPAY_WEBHOOK_SECRET!
  )

  if (!isValid) {
    return Response.json({ error: 'Invalid signature' }, { status: 400 })
  }

  const event = JSON.parse(body)

  switch (event.event) {
    case 'payment.captured':
      await fulfillOrder(event.payload.payment.entity)
      break
    case 'payment.failed':
      await handleFailedPayment(event.payload.payment.entity)
      break
  }

  return Response.json({ received: true })
}
```

## Takeaways

- Server Actions keep your Razorpay secret key server-side always
- The payment flow is two function calls: createOrder then verifyPayment
- Always verify payment signatures server-side — never trust client-side confirmation
- Use webhooks for order fulfillment, not the handler callback alone
- Include a webhook secret and validate signatures to prevent spoofing
- Receipt IDs should be unique — use timestamps or UUIDs
- UPI is 80% of Indian online payments — ensure your Razorpay config enables it by default