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

# Webhooks Overview

> Webhook security, verification, and implementation best practices

## Overview

SkillRise uses webhooks from Clerk and Razorpay to sync data and process payments. Webhooks allow external services to notify your server when events occur.

## Active Webhooks

| Service      | Endpoint         | Events                                         | Purpose                           |
| ------------ | ---------------- | ---------------------------------------------- | --------------------------------- |
| **Clerk**    | `POST /clerk`    | `user.created`, `user.updated`, `user.deleted` | Sync user data to MongoDB         |
| **Razorpay** | `POST /razorpay` | `payment.captured`                             | Complete enrollment after payment |

## Webhook Security

All webhooks use cryptographic signatures to verify authenticity:

* **Clerk**: Uses [Svix](https://www.svix.com/) for signature verification
* **Razorpay**: Uses HMAC-SHA256 signatures

<Warning>
  Never process webhook events without verifying signatures. Attackers can forge requests to your webhook endpoints.
</Warning>

## Clerk Webhooks

### Setup

<Steps>
  <Step title="Configure webhook in Clerk Dashboard">
    1. Go to **Webhooks** → **Add Endpoint**
    2. URL: `https://your-domain.com/clerk`
    3. Subscribe to: `user.created`, `user.updated`, `user.deleted`
    4. Copy the **Signing Secret**
  </Step>

  <Step title="Add secret to environment">
    ```env server/.env theme={null}
    CLERK_WEBHOOK_SECRET=whsec_...
    ```
  </Step>
</Steps>

### Implementation

```javascript server/controllers/webhooks.js theme={null}
import { Webhook } from 'svix'
import User from '../models/User.js'

export const clerkWebhooks = async (req, res) => {
  try {
    const webhook = new Webhook(process.env.CLERK_WEBHOOK_SECRET)

    // Verify signature
    await webhook.verify(JSON.stringify(req.body), {
      'svix-id': req.headers['svix-id'],
      'svix-timestamp': req.headers['svix-timestamp'],
      'svix-signature': req.headers['svix-signature'],
    })

    const { data, type } = req.body

    switch (type) {
      case 'user.created': {
        const userData = {
          _id: data.id,
          email: data.email_addresses[0].email_address,
          name: data.first_name + ' ' + data.last_name,
          imageUrl: data.image_url,
        }
        await User.create(userData)
        res.json({})
        break
      }

      case 'user.updated': {
        const userData = {
          email: data.email_addresses[0].email_address,
          name: data.first_name + ' ' + data.last_name,
          imageUrl: data.image_url,
        }
        await User.findByIdAndUpdate(data.id, userData)
        res.json({})
        break
      }

      case 'user.deleted': {
        await User.findByIdAndDelete(data.id)
        res.json({})
        break
      }

      default:
        res.json({})
    }
  } catch (error) {
    console.error(error)
    res.status(500).json({ 
      success: false, 
      message: 'An unexpected error occurred' 
    })
  }
}
```

### Headers

Clerk sends these headers for verification:

```http theme={null}
svix-id: msg_2Lh9nRv8ypt2BrfXZkcwWsHXbM5
svix-timestamp: 1614265330
svix-signature: v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=
```

## Razorpay Webhooks

### Setup

<Steps>
  <Step title="Configure webhook in Razorpay Dashboard">
    1. Go to **Settings** → **Webhooks** → **Add New Webhook**
    2. URL: `https://your-domain.com/razorpay`
    3. Events: `payment.captured`
    4. Click **Create Webhook**
    5. Copy the **Webhook Secret**
  </Step>

  <Step title="Add secret to environment">
    ```env server/.env theme={null}
    RAZORPAY_WEBHOOK_SECRET=...
    ```
  </Step>
</Steps>

### Implementation

```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 (not parsed JSON)

  // Verify signature using HMAC-SHA256
  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' 
    })
  }

  // Parse event after verification
  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 })
}
```

### Critical: Raw Body Requirement

<Warning>
  Razorpay's HMAC signature is computed over the **exact raw bytes** sent. You must use `express.raw()` middleware for the webhook route, not `express.json()`.
</Warning>

```javascript server/server.js theme={null}
import express from 'express'
import { razorpayWebhooks } from './controllers/webhooks.js'

// CORRECT: Use express.raw() for Razorpay webhook
app.post('/razorpay', 
  express.raw({ type: 'application/json' }), 
  razorpayWebhooks
)

// Then apply express.json() for other routes
app.use(express.json())
```

**Why this matters:**

* `express.json()` parses and re-stringifies the body, changing bytes
* Signature verification will fail if bytes are modified
* `express.raw()` preserves the exact bytes as a Buffer

### Headers

Razorpay sends this header:

```http theme={null}
x-razorpay-signature: 2e3c4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e
```

## Signature Verification Deep Dive

### Clerk (Svix)

Clerk uses the Svix library for webhook signatures:

```javascript theme={null}
import { Webhook } from 'svix'

const webhook = new Webhook(process.env.CLERK_WEBHOOK_SECRET)

// Verify throws an error if signature is invalid
await webhook.verify(JSON.stringify(req.body), {
  'svix-id': req.headers['svix-id'],
  'svix-timestamp': req.headers['svix-timestamp'],
  'svix-signature': req.headers['svix-signature'],
})
```

**What Svix checks:**

1. Signature matches expected HMAC
2. Timestamp is recent (prevents replay attacks)
3. Message ID hasn't been seen before (prevents duplicate processing)

### Razorpay (HMAC-SHA256)

Razorpay uses standard HMAC-SHA256:

```javascript theme={null}
import crypto from 'crypto'

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

const isValid = crypto.timingSafeEqual(
  Buffer.from(expectedSignature),
  Buffer.from(signature)
)
```

**Why `timingSafeEqual`?**

* Prevents timing attacks
* Regular `===` comparison leaks information through timing
* `timingSafeEqual` always takes the same time, regardless of where strings differ

## Webhook Resilience

### Idempotency

Webhooks may be delivered multiple times. Make handlers idempotent:

```javascript theme={null}
// BAD: Always creates a new user
await User.create({ _id: data.id, ... })

// GOOD: Creates only if doesn't exist
await User.findOneAndUpdate(
  { _id: data.id },
  { email: data.email, ... },
  { upsert: true }
)
```

### Error Handling

```javascript theme={null}
export const clerkWebhooks = async (req, res) => {
  try {
    // Verify signature
    await webhook.verify(...)

    // Process event
    await processEvent(req.body)

    // Always return 200 to acknowledge receipt
    res.json({})
  } catch (error) {
    // Log error for debugging
    console.error('Webhook error:', error)

    // Return 500 to trigger retry
    res.status(500).json({ 
      success: false, 
      message: 'An unexpected error occurred' 
    })
  }
}
```

**Best practices:**

* Return `200 OK` to acknowledge successful processing
* Return `400` for invalid signatures (don't retry)
* Return `500` for transient errors (service will retry)
* Log all errors for debugging

### Retry Logic

Both Clerk and Razorpay retry failed webhooks:

| Service      | Retry Schedule                                 | Max Attempts |
| ------------ | ---------------------------------------------- | ------------ |
| **Clerk**    | Exponential backoff (1m, 5m, 30m, 2h, 5h, 10h) | 6            |
| **Razorpay** | Linear backoff (15m, 30m, 1h, 6h, 12h)         | 5            |

## Testing Webhooks Locally

### Using ngrok

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

    Output:

    ```
    Forwarding  https://abc123.ngrok.io -> http://localhost:3000
    ```
  </Step>

  <Step title="Update webhook URLs">
    * Clerk: `https://abc123.ngrok.io/clerk`
    * Razorpay: `https://abc123.ngrok.io/razorpay`
  </Step>

  <Step title="Test webhooks">
    * Clerk: Create a user in Clerk Dashboard
    * Razorpay: Make a test payment
    * Check server logs and database
  </Step>
</Steps>

### Using Webhook Testing Tools

**Clerk:**

* Go to **Webhooks** → Your endpoint → **Testing**
* Click **Send Example** to send test events

**Razorpay:**

* Use [Webhook.site](https://webhook.site) to inspect payloads
* Copy payload and replay via curl:
  ```bash theme={null}
  curl -X POST https://abc123.ngrok.io/razorpay \
    -H "Content-Type: application/json" \
    -H "x-razorpay-signature: ..." \
    -d @payload.json
  ```

## Monitoring Webhooks

### Log All Events

```javascript theme={null}
export const clerkWebhooks = async (req, res) => {
  const { type } = req.body
  
  console.log(`[Webhook] Clerk event: ${type}`, {
    timestamp: new Date().toISOString(),
    headers: req.headers,
  })

  // Process event...
}
```

### Track Failures

```javascript theme={null}
import WebhookLog from '../models/WebhookLog.js'

export const razorpayWebhooks = async (req, res) => {
  try {
    // Verify and process...
    
    await WebhookLog.create({
      service: 'razorpay',
      event: req.body.event,
      status: 'success',
      timestamp: new Date(),
    })
    
    res.json({ received: true })
  } catch (error) {
    await WebhookLog.create({
      service: 'razorpay',
      event: req.body.event,
      status: 'failed',
      error: error.message,
      timestamp: new Date(),
    })
    
    res.status(500).json({ error: 'Failed to process webhook' })
  }
}
```

### Dashboard Monitoring

**Clerk:**

* Go to **Webhooks** → Your endpoint → **Logs**
* View recent deliveries, status codes, and retry attempts

**Razorpay:**

* Go to **Settings** → **Webhooks** → Click your webhook
* View **Recent Deliveries** with response codes

## Common Issues

<AccordionGroup>
  <Accordion title="Signature verification fails">
    **Clerk:**

    * Verify `CLERK_WEBHOOK_SECRET` matches Clerk Dashboard
    * Check headers: `svix-id`, `svix-timestamp`, `svix-signature`
    * Ensure you're stringifying the body: `JSON.stringify(req.body)`

    **Razorpay:**

    * Verify `RAZORPAY_WEBHOOK_SECRET` matches Razorpay Dashboard
    * **Critical**: Use `express.raw()`, not `express.json()`
    * Register webhook route **before** `express.json()` middleware
  </Accordion>

  <Accordion title="Webhooks not received">
    * Check firewall/security groups allow incoming HTTPS
    * Verify URL is correct (no typos, correct protocol)
    * Test with ngrok to rule out network issues
    * Check webhook is subscribed to correct events
  </Accordion>

  <Accordion title="Duplicate events">
    * Make handlers idempotent (use `upsert` instead of `create`)
    * Track processed webhook IDs to skip duplicates
    * Use database unique constraints
  </Accordion>

  <Accordion title="Events out of order">
    * Webhooks may arrive out of order
    * Use timestamps to order events
    * Design handlers to be order-independent
  </Accordion>
</AccordionGroup>

## Security Checklist

<Steps>
  <Step title="Always verify signatures">
    Never process webhook events without cryptographic verification.
  </Step>

  <Step title="Use HTTPS in production">
    Webhook providers require HTTPS for security.
  </Step>

  <Step title="Validate event structure">
    Use Zod or similar to validate webhook payloads before processing.
  </Step>

  <Step title="Rate limiting">
    Apply rate limits to webhook endpoints to prevent abuse.
  </Step>

  <Step title="Secret rotation">
    Rotate webhook secrets periodically (every 90 days).
  </Step>

  <Step title="Monitor failures">
    Set up alerts for webhook failures (email, Slack, etc.).
  </Step>
</Steps>

## Resources

<CardGroup cols={2}>
  <Card title="Clerk Webhooks" icon="shield-halved" href="https://clerk.com/docs/integrations/webhooks">
    Clerk webhook documentation
  </Card>

  <Card title="Razorpay Webhooks" icon="credit-card" href="https://razorpay.com/docs/webhooks">
    Razorpay webhook guide
  </Card>

  <Card title="Svix" icon="webhook" href="https://www.svix.com/">
    Svix webhook platform (used by Clerk)
  </Card>

  <Card title="ngrok" icon="globe" href="https://ngrok.com">
    Expose localhost for testing
  </Card>
</CardGroup>
