Svix webhook verification failed (Clerk, Resend): “No matching signature found” fixes

Clerk, Resend and many other platforms send webhooks through Svix. Each request carries three headers: svix-id, svix-timestamp and svix-signature. The signed content is {svix-id}.{svix-timestamp}.{raw body}. The key is your endpoint secret: drop the whsec_ prefix and base64-decode the rest. The result is HMAC-SHA256, base64-encoded. svix-signature is a space-separated list like v1,<base64> v1,<base64> (more than one during secret rotation). If no entry matches, the Svix libraries throw WebhookVerificationError: No matching signature found.

Common causes

  1. Parsed body. Verification needs the exact raw body. JSON.stringify(req.body) after express.json(), or await req.json() in Next.js, changes whitespace and key order. Use express.raw() or await req.text().
  2. Wrong secret. Each endpoint has its own signing secret (whsec_...), and dev and prod instances differ. Copy it from that endpoint's page in the Clerk or Resend dashboard. API keys won't work.
  3. Hand-rolled HMAC with the wrong key. The key is the base64-decoded part after whsec_, not the whole string as UTF-8 text. The digest is base64, not hex.
  4. Headers missing. Some proxies and frameworks drop or rename headers. Passing only svix-signature gives "Missing required headers". Pass all three.
  5. Timestamp too old or too new. The libraries reject timestamps more than 5 minutes from your clock ("Message timestamp too old" / "too new"). Fix the server clock. Replaying an old request by hand will also fail.
  6. Middleware intercepts the route. In Next.js with Clerk, make the webhook route public in clerkMiddleware so it isn't redirected or blocked before your handler runs.

Node (Express, svix package)

const { Webhook } = require('svix');
app.post('/webhooks/clerk', express.raw({ type: 'application/json' }), (req, res) => {
  const wh = new Webhook(process.env.WEBHOOK_SECRET);              // whsec_...
  let evt;
  try {
    evt = wh.verify(req.body.toString('utf8'), {
      'svix-id': req.get('svix-id'),
      'svix-timestamp': req.get('svix-timestamp'),
      'svix-signature': req.get('svix-signature'),
    });
  } catch (err) { return res.status(400).send('invalid signature'); }
  // evt is the parsed JSON payload; de-duplicate on svix-id
  res.sendStatus(200);
});

Next.js route handler: const payload = await req.text(); then wh.verify(payload, Object.fromEntries(req.headers)). Clerk also ships verifyWebhook(req) in @clerk/nextjs/webhooks, which does the same check with CLERK_WEBHOOK_SIGNING_SECRET.

Python (FastAPI, svix package)

from fastapi import Request, HTTPException
from svix.webhooks import Webhook, WebhookVerificationError

@app.post('/webhooks/clerk')
async def clerk_webhook(request: Request):
    payload = await request.body()                               # raw bytes
    try:
        evt = Webhook(os.environ['WEBHOOK_SECRET']).verify(payload, dict(request.headers))
    except WebhookVerificationError:
        raise HTTPException(status_code=400, detail='invalid signature')
    return {'ok': True}

Without the library

const [id, ts, body] = [headers['svix-id'], headers['svix-timestamp'], rawBody];
const key = Buffer.from(secret.split('_')[1], 'base64');
const expected = crypto.createHmac('sha256', key).update(`${id}.${ts}.${body}`).digest('base64');
const ok = headers['svix-signature'].split(' ').some(p => {
  const s = p.split(',')[1] || '';
  return s.length === expected.length && crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected));
});   // also reject if |now - ts| > 300 s

The guide is Stripe-only. The kit is a self-hosted Cloudflare Worker relay that adds retries, replay and a dead-letter list in front of your endpoint. Its built-in schemes cover Stripe, Lemon Squeezy, Shopify, Paddle Billing, Square, Twilio (status callbacks) and GitHub. Svix webhooks (Clerk, Resend) aren't one of them, so you would add a small verifier like the one above.

Fix Your Stripe Webhooks guide ($9) · Self-Hosted Webhook Relay Kit ($19)

Free download: Webhook debugging cheat sheet.

Fighting webhooks on more than one platform? The Fix Your Stripe Webhooks guide ($9) covers raw-body and signature problems in depth, and the Self-Hosted Webhook Relay Kit ($19) adds retries, replay and a dead-letter list in front of any endpoint. More fixes: all webhook error pages.