Skip to main content

Overview

SkillRise uses Stripe for processing course payments (Note: The codebase actually uses Razorpay as the payment provider, not Stripe. This documentation covers the Razorpay implementation).
The README mentions Stripe, but the actual implementation uses Razorpay. This is common in Indian e-learning platforms. The documentation below covers the actual Razorpay implementation.

Features

  • Secure checkout: Razorpay embedded payment UI
  • Multiple payment methods: Cards, UPI, Netbanking, Wallets
  • Webhook verification: HMAC-SHA256 signature validation
  • Order tracking: Purchase status management
  • Fallback enrollment: Webhook ensures enrollment even if frontend fails

Environment Variables

Server Configuration

Add these to your server/.env file:
server/.env
RAZORPAY_KEY_ID=rzp_test_...
RAZORPAY_KEY_SECRET=...
RAZORPAY_WEBHOOK_SECRET=...
CURRENCY=INR
Get your keys from the Razorpay Dashboard. Create an account and generate API keys under SettingsAPI Keys.

Setup Instructions

1

Create Razorpay Account

  1. Go to Razorpay
  2. Sign up for an account
  3. Complete KYC verification (required for live mode)
  4. Start with Test Mode for development
2

Generate API Keys

  1. Go to SettingsAPI Keys
  2. Click Generate Test Keys (or Generate Live Keys for production)
  3. Copy the Key ID and Key Secret
  4. Add them to server/.env:
    RAZORPAY_KEY_ID=rzp_test_...
    RAZORPAY_KEY_SECRET=...
    
3

Configure Webhooks

  1. Go to SettingsWebhooks
  2. Click Add New Webhook
  3. Enter your webhook URL:
    • Development: Use ngrokhttps://your-ngrok-url.ngrok.io/razorpay
    • Production: https://your-domain.com/razorpay
  4. Select events:
    • payment.captured
  5. Click Create Webhook
  6. Copy the Webhook Secret and add it to server/.env:
    RAZORPAY_WEBHOOK_SECRET=...
    
4

Install Dependencies

cd server
npm install razorpay crypto

Payment Flow

The payment flow consists of three steps:
  1. Create Order: Backend creates a Razorpay order
  2. Process Payment: Frontend opens Razorpay checkout modal
  3. Verify Payment: Backend verifies signature and completes enrollment
  4. Webhook Fallback: Razorpay webhook ensures enrollment even if step 3 fails

1. Create Order

When a user initiates a purchase, the backend creates a Razorpay order:
server/services/payments/razorpay.service.js
import Razorpay from 'razorpay'
import Purchase from '../../models/Purchase.js'

export const createOrder = async ({ purchaseId, amount, courseTitle }) => {
  const razorpayInstance = new Razorpay({
    key_id: process.env.RAZORPAY_KEY_ID,
    key_secret: process.env.RAZORPAY_KEY_SECRET,
  })

  const currency = process.env.CURRENCY || 'INR'

  const order = await razorpayInstance.orders.create({
    amount: Math.round(amount * 100), // convert to paise
    currency,
    receipt: purchaseId.toString(),
    notes: {
      purchaseId: purchaseId.toString(),
      courseTitle,
    },
  })

  // Store the Razorpay order id on the Purchase
  await Purchase.findByIdAndUpdate(purchaseId, { 
    providerOrderId: order.id 
  })

  return {
    orderId: order.id,
    keyId: process.env.RAZORPAY_KEY_ID,
  }
}
API Endpoint:
POST /api/user/purchase

Request:
{
  "courseId": "64abc123..."
}

Response:
{
  "success": true,
  "orderId": "order_...",
  "keyId": "rzp_test_...",
  "amount": 2999,
  "currency": "INR"
}

2. Frontend Integration

Open Razorpay checkout modal on the frontend:
client/src/components/Checkout.jsx
const handlePayment = async () => {
  // Create order
  const response = await fetch('/api/user/purchase', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ courseId }),
  })
  const { orderId, keyId, amount, currency } = await response.json()

  // Open Razorpay checkout
  const options = {
    key: keyId,
    amount: amount,
    currency: currency,
    name: 'SkillRise',
    description: 'Course Purchase',
    order_id: orderId,
    handler: async (response) => {
      // Verify payment on backend
      await fetch('/api/user/verify-razorpay', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          razorpay_order_id: response.razorpay_order_id,
          razorpay_payment_id: response.razorpay_payment_id,
          razorpay_signature: response.razorpay_signature,
        }),
      })
    },
    theme: { color: '#3b82f6' },
  }

  const razorpay = new window.Razorpay(options)
  razorpay.open()
}
Load Razorpay script:
client/index.html
<script src="https://checkout.razorpay.com/v1/checkout.js"></script>

3. Verify Payment Signature

The backend verifies the payment signature using HMAC-SHA256:
server/services/payments/razorpay.service.js
import crypto from 'crypto'

export const verifyPayment = ({ orderId, paymentId, signature }) => {
  const expected = crypto
    .createHmac('sha256', process.env.RAZORPAY_KEY_SECRET)
    .update(`${orderId}|${paymentId}`)
    .digest('hex')
    
  return crypto.timingSafeEqual(
    Buffer.from(expected), 
    Buffer.from(signature)
  )
}
API Endpoint:
POST /api/user/verify-razorpay

Request:
{
  "razorpay_order_id": "order_...",
  "razorpay_payment_id": "pay_...",
  "razorpay_signature": "..."
}

Response:
{
  "success": true,
  "message": "Payment verified and enrollment complete"
}

4. Webhook Implementation

Webhook acts as a reliable fallback if the frontend verification fails (network drop, browser close, etc.):
server/controllers/webhooks.js
import crypto from 'crypto'
import { completePurchase } from '../services/payments/order.service.js'

export const razorpayWebhooks = async (req, res) => {
  const signature = req.headers['x-razorpay-signature']
  const rawBody = req.body // raw Buffer (express.raw middleware)

  const expectedSignature = crypto
    .createHmac('sha256', process.env.RAZORPAY_WEBHOOK_SECRET)
    .update(rawBody)
    .digest('hex')

  if (
    !signature ||
    !crypto.timingSafeEqual(
      Buffer.from(expectedSignature), 
      Buffer.from(signature)
    )
  ) {
    return res.status(400).json({ 
      error: 'Invalid Razorpay webhook signature' 
    })
  }

  const event = JSON.parse(rawBody.toString())

  if (event.event === 'payment.captured') {
    const payment = event.payload?.payment?.entity
    const purchaseId = payment?.notes?.purchaseId
    const paymentId = payment?.id

    if (purchaseId && paymentId) {
      await completePurchase(purchaseId, paymentId)
    }
  }

  res.json({ received: true })
}
Register webhook route:
server/server.js
import { razorpayWebhooks } from './controllers/webhooks.js'

// IMPORTANT: Use express.raw() for Razorpay webhooks
// Signature verification requires raw bytes, not parsed JSON
app.post('/razorpay', 
  express.raw({ type: 'application/json' }), 
  razorpayWebhooks
)

// Then apply express.json() for other routes
app.use(express.json())
Razorpay webhook signature is computed over the exact raw bytes. The webhook route must use express.raw() middleware, not express.json(). Apply express.json() after the webhook route.

Rate Limiting

Protect payment endpoints from abuse:
server/server.js
import { rateLimit, ipKeyGenerator } from 'express-rate-limit'

const paymentLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  limit: 10, // 10 requests per window
  keyGenerator: (req) => req.auth?.userId || ipKeyGenerator(req),
  message: { 
    success: false, 
    message: 'Too many payment attempts. Please try again later.' 
  },
})

app.use('/api/user/purchase', paymentLimiter)
app.use('/api/user/verify-razorpay', paymentLimiter)

Testing Payments

Test Cards

Razorpay provides test cards for development:
Card NumberTypeResult
4111 1111 1111 1111VisaSuccess
5555 5555 5555 4444MastercardSuccess
4000 0000 0000 0002VisaDeclined
Test card details:
  • CVV: Any 3 digits
  • Expiry: Any future date
  • Name: Any name

Test UPI

Use success@razorpay as the UPI ID in test mode.

Testing Webhooks Locally

1

Install ngrok

npm install -g ngrok
2

Start your server

cd server
npm run server
3

Expose localhost

ngrok http 3000
Copy the HTTPS URL (e.g., https://abc123.ngrok.io)
4

Update Razorpay webhook URL

In Razorpay Dashboard → Settings → Webhooks:
https://abc123.ngrok.io/razorpay
5

Test payment

Make a test purchase using a test card. Check:
  • Server logs for webhook event
  • MongoDB for completed purchase
  • User enrollment in course

Production Checklist

1

Switch to Live Mode

  • Complete KYC verification in Razorpay Dashboard
  • Generate Live API Keys
  • Update RAZORPAY_KEY_ID and RAZORPAY_KEY_SECRET with live keys
2

Update Webhook URL

Replace ngrok URL with your production domain:
https://api.yourplatform.com/razorpay
3

Enable HTTPS

Razorpay requires HTTPS for webhooks in production. Use:
  • Let’s Encrypt (free SSL)
  • Cloudflare (free SSL + CDN)
  • Your hosting provider’s SSL
4

Test End-to-End

  • Make a real small payment (₹1)
  • Verify enrollment completes
  • Check webhook logs
  • Test refund flow

Common Issues

  • Ensure you’re using express.raw() middleware for the webhook route
  • Verify RAZORPAY_WEBHOOK_SECRET matches the secret in Razorpay Dashboard
  • Check that the webhook route is registered before express.json()
  • Confirm headers x-razorpay-signature is being sent
  • Check server logs for errors in completePurchase() function
  • Verify MongoDB connection is active
  • Ensure the purchaseId is correctly stored in Razorpay order notes
  • Check that the webhook is subscribed to payment.captured event
  • Razorpay amounts are in paise (smallest currency unit)
  • Convert rupees to paise: amount * 100
  • Example: ₹2999 → 299900 paise
  • Both frontend verification and webhook can trigger enrollment
  • Ensure completePurchase() is idempotent (checks if already completed)
  • Add unique constraints on Purchase model

Security Best Practices

Verify Signatures

Always verify webhook signatures using crypto.timingSafeEqual() to prevent timing attacks.

Use HTTPS

Never send API keys or handle payments over HTTP. Always use HTTPS in production.

Rate Limiting

Apply strict rate limits to payment endpoints to prevent abuse and fraud attempts.

Secure Keys

Never commit API keys to git. Use environment variables and keep secrets secure.

Resources