Skip to main content
CodePlanet Docs

Razorpay Integration

Secure payment processing

CodePlanet uses Razorpay for secure payment processing. This document covers how payments work, the integration architecture, and troubleshooting common issues.

Overview

Razorpay handles all payment processing for CodePlanet subscriptions. We support:

  • πŸ’³ Credit/Debit Cards β€” Visa, Mastercard, RuPay
  • 🏦 Net Banking β€” All major Indian banks
  • πŸ“± UPI β€” Google Pay, PhonePe, Paytm, etc.
  • πŸ’° Wallets β€” Paytm, PhonePe, Amazon Pay

Payment Flow

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   User clicks    │────▢│  Create Order    │────▢│  Razorpay        β”‚
β”‚   "Subscribe"    β”‚     β”‚  (API call)      β”‚     β”‚  Checkout opens  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                           β”‚
                                                           β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Subscription   │◀────│  Verify          │◀────│  User completes  β”‚
β”‚   activated      β”‚     β”‚  Signature       β”‚     β”‚  payment         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Step 1: Order Creation

When a user initiates a payment, we create a Razorpay order:

// POST /api/v1/payments/create
const order = await razorpay.orders.create({
  amount: planAmount * 100, // Amount in paise
  currency: "INR",
  receipt: `order_${userId}_${Date.now()}`,
  notes: {
    userId,
    planId,
  },
});

Important: Amount is always determined server-side from the plan ID. We never trust client-provided amounts.

Step 2: Checkout

The Razorpay checkout modal opens in the browser:

const options = {
  key: process.env.NEXT_PUBLIC_RAZORPAY_KEY_ID,
  amount: order.amount,
  currency: order.currency,
  name: "CodePlanet",
  description: "Pro Subscription",
  order_id: order.id,
  handler: async (response) => {
    // Verify payment
    await verifyPayment(response);
  },
};
 
const razorpay = new Razorpay(options);
razorpay.open();

Step 3: Signature Verification

After payment, we verify the signature to ensure authenticity:

// Server-side verification
import crypto from "crypto";
 
const expectedSignature = crypto
  .createHmac("sha256", process.env.RAZORPAY_KEY_SECRET)
  .update(`${orderId}|${paymentId}`)
  .digest("hex");
 
// Timing-safe comparison to prevent timing attacks
const isValid = crypto.timingSafeEqual(
  Buffer.from(signature),
  Buffer.from(expectedSignature)
);

Security Note: We use crypto.timingSafeEqual to prevent timing attacks on signature comparison.

Webhook Integration

Razorpay sends webhook events for payment lifecycle events:

Supported Events

EventDescription
payment.authorizedPayment authorized, awaiting capture
payment.capturedPayment successfully captured
payment.failedPayment failed
refund.createdRefund initiated
subscription.activatedSubscription started
subscription.cancelledSubscription cancelled

Webhook Endpoint

// POST /api/v1/payments/webhook
export async function POST(request: Request) {
  const body = await request.text();
  const signature = request.headers.get("x-razorpay-signature");
 
  // Verify webhook signature
  const expectedSignature = crypto
    .createHmac("sha256", process.env.RAZORPAY_WEBHOOK_SECRET)
    .update(body)
    .digest("hex");
 
  if (!timingSafeEqual(signature, expectedSignature)) {
    return Response.json({ error: "Invalid signature" }, { status: 401 });
  }
 
  const event = JSON.parse(body);
  
  switch (event.event) {
    case "payment.captured":
      await activateSubscription(event.payload.payment.entity);
      break;
    case "payment.failed":
      await handleFailedPayment(event.payload.payment.entity);
      break;
  }
 
  return Response.json({ status: "ok" });
}

UPI Direct Integration

For faster UPI payments, we support direct UPI intent:

// POST /api/v1/payments/upi
const upiIntent = {
  orderId: order.id,
  amount: order.amount,
  upiId: process.env.RAZORPAY_UPI_ID,
  qrCode: await generateQRCode(upiLink),
};

Users can either:

  1. Scan the QR code with any UPI app
  2. Click the UPI intent link on mobile

Security Measures

1. Server-Side Pricing

Amount is always calculated server-side from the plan ID:

const VALID_PLANS = {
  developer: 29900,  // β‚Ή299
  pro: 79900,        // β‚Ή799
} as const;
 
const amount = VALID_PLANS[planId];
if (!amount) throw new Error("Invalid plan");

2. Input Validation

All payment endpoints validate:

  • UUID format for user IDs
  • Valid plan IDs only
  • Signature presence and format

3. Rate Limiting

Payment endpoints are rate-limited:

  • 10 orders per hour per user
  • 3 verification attempts per order

4. Idempotency

Each order has a unique receipt to prevent duplicate charges.

Testing

In test mode, use Razorpay test cards:

Card NumberTypeResult
4111 1111 1111 1111VisaSuccess
5267 3181 8797 5449MastercardSuccess
4000 0000 0000 0002AnyFailure

Test UPI ID: success@razorpay

Error Handling

Common payment errors and their handling:

Error CodeMeaningUser Message
BAD_REQUEST_ERRORInvalid parameters"Please check your payment details"
GATEWAY_ERRORBank/gateway issue"Payment failed, please try again"
SERVER_ERROROur server error"Something went wrong, please retry"

Subscription States

StateDescription
activeSubscription is active and paid
pendingPayment initiated but not completed
cancelledUser cancelled subscription
expiredSubscription period ended

Troubleshooting

Payment Not Completing

  1. Check browser console for errors
  2. Ensure popup blocker is disabled
  3. Try a different payment method
  4. Contact support with order ID

Subscription Not Activating

  1. Check webhook delivery in Razorpay dashboard
  2. Verify webhook secret is correct
  3. Check server logs for errors

Refund Issues

Refunds are processed within 5-7 business days. Contact support with:

  • Order ID
  • Payment ID
  • Reason for refund

Next Steps