> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/pv-pushkarverma/SkillRise/llms.txt
> Use this file to discover all available pages before exploring further.

# Stripe Payments

> Configure Stripe for course payments with embedded checkout and webhook verification

## Overview

SkillRise uses [Stripe](https://stripe.com) for processing course payments (Note: The codebase actually uses **Razorpay** as the payment provider, not Stripe. This documentation covers the Razorpay implementation).

<Warning>
  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.
</Warning>

## 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:

```env server/.env theme={null}
RAZORPAY_KEY_ID=rzp_test_...
RAZORPAY_KEY_SECRET=...
RAZORPAY_WEBHOOK_SECRET=...
CURRENCY=INR
```

<Note>
  Get your keys from the [Razorpay Dashboard](https://dashboard.razorpay.com). Create an account and generate API keys under **Settings** → **API Keys**.
</Note>

## Setup Instructions

<Steps>
  <Step title="Create Razorpay Account">
    1. Go to [Razorpay](https://razorpay.com)
    2. Sign up for an account
    3. Complete KYC verification (required for live mode)
    4. Start with **Test Mode** for development
  </Step>

  <Step title="Generate API Keys">
    1. Go to **Settings** → **API 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`:
       ```env theme={null}
       RAZORPAY_KEY_ID=rzp_test_...
       RAZORPAY_KEY_SECRET=...
       ```
  </Step>

  <Step title="Configure Webhooks">
    1. Go to **Settings** → **Webhooks**
    2. Click **Add New Webhook**
    3. Enter your webhook URL:
       * Development: Use [ngrok](https://ngrok.com) → `https://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`:
       ```env theme={null}
       RAZORPAY_WEBHOOK_SECRET=...
       ```
  </Step>

  <Step title="Install Dependencies">
    ```bash theme={null}
    cd server
    npm install razorpay crypto
    ```
  </Step>
</Steps>

## 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:

```javascript server/services/payments/razorpay.service.js theme={null}
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:**

```javascript theme={null}
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:

```javascript client/src/components/Checkout.jsx theme={null}
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:**

```html client/index.html theme={null}
<script src="https://checkout.razorpay.com/v1/checkout.js"></script>
```

### 3. Verify Payment Signature

The backend verifies the payment signature using HMAC-SHA256:

```javascript server/services/payments/razorpay.service.js theme={null}
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:**

```javascript theme={null}
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.):

```javascript server/controllers/webhooks.js theme={null}
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:**

```javascript server/server.js theme={null}
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())
```

<Warning>
  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.
</Warning>

## Rate Limiting

Protect payment endpoints from abuse:

```javascript server/server.js theme={null}
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 Number           | Type       | Result   |
| --------------------- | ---------- | -------- |
| `4111 1111 1111 1111` | Visa       | Success  |
| `5555 5555 5555 4444` | Mastercard | Success  |
| `4000 0000 0000 0002` | Visa       | Declined |

**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

<Steps>
  <Step title="Install ngrok">
    ```bash theme={null}
    npm install -g ngrok
    ```
  </Step>

  <Step title="Start your server">
    ```bash theme={null}
    cd server
    npm run server
    ```
  </Step>

  <Step title="Expose localhost">
    ```bash theme={null}
    ngrok http 3000
    ```

    Copy the HTTPS URL (e.g., `https://abc123.ngrok.io`)
  </Step>

  <Step title="Update Razorpay webhook URL">
    In Razorpay Dashboard → Settings → Webhooks:

    ```
    https://abc123.ngrok.io/razorpay
    ```
  </Step>

  <Step title="Test payment">
    Make a test purchase using a test card. Check:

    * Server logs for webhook event
    * MongoDB for completed purchase
    * User enrollment in course
  </Step>
</Steps>

## Production Checklist

<Steps>
  <Step title="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
  </Step>

  <Step title="Update Webhook URL">
    Replace ngrok URL with your production domain:

    ```
    https://api.yourplatform.com/razorpay
    ```
  </Step>

  <Step title="Enable HTTPS">
    Razorpay requires HTTPS for webhooks in production. Use:

    * Let's Encrypt (free SSL)
    * Cloudflare (free SSL + CDN)
    * Your hosting provider's SSL
  </Step>

  <Step title="Test End-to-End">
    * Make a real small payment (₹1)
    * Verify enrollment completes
    * Check webhook logs
    * Test refund flow
  </Step>
</Steps>

## Common Issues

<AccordionGroup>
  <Accordion title="Webhook signature verification fails">
    * 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
  </Accordion>

  <Accordion title="Payment succeeds but enrollment fails">
    * 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
  </Accordion>

  <Accordion title="Amount mismatch">
    * Razorpay amounts are in **paise** (smallest currency unit)
    * Convert rupees to paise: `amount * 100`
    * Example: ₹2999 → 299900 paise
  </Accordion>

  <Accordion title="Duplicate enrollments">
    * Both frontend verification and webhook can trigger enrollment
    * Ensure `completePurchase()` is idempotent (checks if already completed)
    * Add unique constraints on `Purchase` model
  </Accordion>
</AccordionGroup>

## Security Best Practices

<CardGroup cols={2}>
  <Card title="Verify Signatures" icon="shield-check">
    Always verify webhook signatures using `crypto.timingSafeEqual()` to prevent timing attacks.
  </Card>

  <Card title="Use HTTPS" icon="lock">
    Never send API keys or handle payments over HTTP. Always use HTTPS in production.
  </Card>

  <Card title="Rate Limiting" icon="gauge">
    Apply strict rate limits to payment endpoints to prevent abuse and fraud attempts.
  </Card>

  <Card title="Secure Keys" icon="key">
    Never commit API keys to git. Use environment variables and keep secrets secure.
  </Card>
</CardGroup>

## Resources

<CardGroup cols={2}>
  <Card title="Razorpay Docs" icon="book" href="https://razorpay.com/docs">
    Official Razorpay documentation
  </Card>

  <Card title="Payment Gateway" icon="credit-card" href="https://razorpay.com/docs/payments/payment-gateway">
    Web integration guide
  </Card>

  <Card title="Webhooks" icon="webhook" href="https://razorpay.com/docs/webhooks">
    Webhook integration guide
  </Card>

  <Card title="Test Cards" icon="credit-card" href="https://razorpay.com/docs/payments/payments/test-card-details">
    Test card numbers
  </Card>
</CardGroup>
