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
import { verifyWebhookSignature } from '@vgpprasad91/churnkit-sdk'
verifyWebhookSignature (
rawBody : string ,
signature : string ,
secret : string
): Promise < boolean >
Parameters
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.
The value of the X-ChurnKit-Signature header. Format: sha256=<hex>.
Your webhook signing secret from the FlowsBuilt Console.
Examples
Next.js App Router
// 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
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)
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
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. // ❌ 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 )
Using a body parser middleware
Express’s express.json() middleware consumes and parses the body. Use express.raw() for webhook routes instead. // ❌ 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 )
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.