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

# verifyWebhookSignature()

> Verify that a webhook was sent by ChurnKit and hasn't been tampered with.

## Overview

ChurnKit signs every webhook payload with HMAC-SHA256 using your webhook secret. Always verify the signature before processing the payload.

The SDK exports `verifyWebhookSignature` as a standalone function — no `ChurnKit` instance required.

## Signature

```typescript theme={null}
import { verifyWebhookSignature } from '@vgpprasad91/churnkit-sdk'

verifyWebhookSignature(
  rawBody: string,
  signature: string,
  secret: string
): Promise<boolean>
```

## Parameters

<ParamField path="rawBody" type="string" required>
  The **raw, unparsed** request body as a string. Do not parse it as JSON before passing it here — the signature is computed over the raw bytes.
</ParamField>

<ParamField path="signature" type="string" required>
  The value of the `X-ChurnKit-Signature` header. Format: `sha256=<hex>`.
</ParamField>

<ParamField path="secret" type="string" required>
  Your webhook signing secret from the FlowsBuilt Console.
</ParamField>

## Examples

### Next.js App Router

```typescript theme={null}
// app/api/webhooks/churnkit/route.ts
import { verifyWebhookSignature } from '@vgpprasad91/churnkit-sdk'

export async function POST(req: Request) {
  // Read body as raw text BEFORE parsing
  const rawBody = await req.text()
  const signature = req.headers.get('x-churnkit-signature') ?? ''

  const valid = await verifyWebhookSignature(
    rawBody,
    signature,
    process.env.CHURNKIT_WEBHOOK_SECRET!
  )

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

  const payload = JSON.parse(rawBody)

  switch (payload.event) {
    case 'churn_risk_alert':
      await handleChurnAlert(payload)
      break
  }

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

### Express

```typescript theme={null}
import express from 'express'
import { verifyWebhookSignature } from '@vgpprasad91/churnkit-sdk'

const app = express()

// Use raw body parser for webhook routes
app.post('/hooks/churnkit', express.raw({ type: 'application/json' }), async (req, res) => {
  const rawBody = req.body.toString('utf8')
  const signature = req.headers['x-churnkit-signature'] as string ?? ''

  const valid = await verifyWebhookSignature(
    rawBody,
    signature,
    process.env.CHURNKIT_WEBHOOK_SECRET!
  )

  if (!valid) return res.status(401).json({ error: 'Invalid signature' })

  const payload = JSON.parse(rawBody)
  // handle payload...

  res.json({ ok: true })
})
```

### Hono (Cloudflare Workers)

```typescript theme={null}
import { Hono } from 'hono'
import { verifyWebhookSignature } from '@vgpprasad91/churnkit-sdk'

const app = new Hono<{ Bindings: { CHURNKIT_WEBHOOK_SECRET: string } }>()

app.post('/hooks/churnkit', async (c) => {
  const rawBody = await c.req.text()
  const signature = c.req.header('x-churnkit-signature') ?? ''

  const valid = await verifyWebhookSignature(rawBody, signature, c.env.CHURNKIT_WEBHOOK_SECRET)
  if (!valid) return c.json({ error: 'Invalid signature' }, 401)

  const payload = JSON.parse(rawBody)
  // handle...

  return c.json({ ok: true })
})
```

## How the signature is computed

ChurnKit computes:

```
HMAC-SHA256(key=webhookSecret, data=rawBody)
```

And sends the hex digest as:

```
X-ChurnKit-Signature: sha256=<hex_digest>
```

The SDK's `verifyWebhookSignature` performs the same computation and compares with a constant-time comparison to prevent timing attacks.

## Common mistakes

<AccordionGroup>
  <Accordion title="Parsing JSON before verifying">
    The signature is over the **raw bytes**. If you call `JSON.parse()` before verifying, the comparison will fail because JSON serialization can change whitespace and key order.

    ```typescript theme={null}
    // ❌ Wrong
    const payload = await req.json()
    await verifyWebhookSignature(JSON.stringify(payload), sig, secret)

    // ✅ Correct
    const rawBody = await req.text()
    await verifyWebhookSignature(rawBody, sig, secret)
    const payload = JSON.parse(rawBody)
    ```
  </Accordion>

  <Accordion title="Using a body parser middleware">
    Express's `express.json()` middleware consumes and parses the body. Use `express.raw()` for webhook routes instead.

    ```typescript theme={null}
    // ❌ Wrong — body already parsed by middleware
    app.use(express.json())
    app.post('/hooks/churnkit', handler)

    // ✅ Correct — raw body for this route
    app.post('/hooks/churnkit', express.raw({ type: '*/*' }), handler)
    ```
  </Accordion>

  <Accordion title="Missing the secret in environment">
    If `process.env.CHURNKIT_WEBHOOK_SECRET` is undefined, verification always returns false.
    Check your environment variables and ensure the secret is set.
  </Accordion>
</AccordionGroup>
