Documentation Menu

Clerk Webhook Signature Verification Failed? Practical Fixes

Problem Introduction

Clerk webhook requests arrive but are rejected as invalid signatures. Clerk uses Svix to deliver webhooks, so the fix follows the Svix signature pattern.

Why It Happens

  • Raw body was mutated by JSON middleware before verification
  • Wrong signing secret — must come from Clerk Dashboard → Webhooks, not API keys
  • Incorrect signature header extraction (webhook-signature, not Authorization)
  • Unsafe string compare instead of timing-safe

Step-by-Step Fix

  1. 1In Clerk Dashboard → Webhooks, copy the Signing Secret (starts with whsec_).
  2. 2Use express.raw() or Next.js request.text() to capture raw body — before any JSON parsing.
  3. 3Extract webhook-id, webhook-timestamp, webhook-signature headers.
  4. 4Follow the Svix verification pattern: build signed string, decode secret, HMAC, compare.
  5. 5Test with a known-good event from the Clerk Webhooks dashboard.

Working Code

Copy-paste verified examples. Use the tab that matches your stack.

javascript
// app/api/webhooks/clerk/route.js
import { Webhook } from 'svix';

export async function POST(req) {
  const webhookSecret = process.env.CLERK_WEBHOOK_SECRET; // whsec_...

  const svixId        = req.headers.get('webhook-id');
  const svixTimestamp = req.headers.get('webhook-timestamp');
  const svixSignature = req.headers.get('webhook-signature');

  if (!svixId || !svixTimestamp || !svixSignature) {
    return new Response('Missing Svix headers', { status: 400 });
  }

  // MUST get raw body as text — not parsed JSON
  const body = await req.text();

  let event;
  try {
    const wh = new Webhook(webhookSecret);
    event = wh.verify(body, {
      'webhook-id': svixId,
      'webhook-timestamp': svixTimestamp,
      'webhook-signature': svixSignature,
    });
  } catch (err) {
    console.error('Clerk webhook verification failed:', err.message);
    return new Response('Invalid signature', { status: 401 });
  }

  console.log('Clerk event:', event.type, event.data?.id);
  return new Response(JSON.stringify({ received: true }), { status: 200 });
}

Common Mistakes

  • Comparing signatures as plain strings
  • Re-serializing parsed JSON before hashing
  • Mixing staging and production secrets

Debugging Workflow

Header capture → raw payload hash → expected vs received comparison → replay test.

Preventive Best Practices

  • Centralize provider-specific signature modules
  • Log mismatch reason categories
  • Alert on mismatch spikes

Works with webhooks and other async event systems (including AI callbacks). Instead of guessing, inspecting the exact payload and headers can help debug faster.

Try the free webhook tester

Was this page helpful?

Your feedback helps us improve the docs.