sickn33/agentic-awesome-skills · critical

Invalid webhook signature

Error message

Invalid webhook signature

What it means

validateHMAC reached the final check: the header is well-formed, but after the length comparison and crypto.timingSafeEqual it does not equal the HMAC computed over (req as any).rawBody with the configured app secret. Either the secret differs from the one Meta used to sign, or the rawBody buffer being verified is not the exact byte sequence Meta signed.

Source

Thrown at skills/whatsapp-cloud-api/assets/boilerplate/nodejs/src/webhook-handler.ts:52

    const expectedSignature =
      'sha256=' +
      crypto.createHmac('sha256', appSecret).update(rawBody).digest('hex');

    if (!SIGNATURE_RE.test(signature)) {
      console.warn('Invalid webhook signature format');
      res.sendStatus(401);
      return;
    }

    const signatureBuffer = Buffer.from(signature, 'utf8');
    const expectedSignatureBuffer = Buffer.from(expectedSignature, 'utf8');
    const isValid =
      signatureBuffer.length === expectedSignatureBuffer.length &&
      crypto.timingSafeEqual(signatureBuffer, expectedSignatureBuffer);

    if (!isValid) {
      console.warn('Invalid webhook signature');
      res.sendStatus(401);
      return;
    }

    next();
  };
}

/**
 * Middleware para capturar o raw body antes do JSON parse.
 * Necessario para validacao HMAC.
 */
export function rawBodyMiddleware(req: Request, _res: Response, buf: Buffer): void {
  (req as any).rawBody = buf;
}

/**
 * Handler de verificacao do webhook (GET).

View on GitHub (pinned to 58d857988f)

Solutions

  1. Confirm the process's app secret equals the Meta App Secret exactly
  2. Capture the raw body: app.use(express.json({ verify: (req, res, buf) => { (req as any).rawBody = buf; } })) and mount validateHMAC after it
  3. After any secret rotation, redeploy every instance so verification uses the new value

Example fix

// before — body parser consumes the stream, rawBody is undefined
app.use(express.json());

// after — capture raw bytes for HMAC verification
app.use(express.json({
  verify: (req, res, buf) => { (req as any).rawBody = buf; }
}));
Defensive patterns

Strategy: validation

Validate before calling

app.use(express.json({ verify: (req, _res, buf) => { (req as any).rawBody = buf; } }));
if (!Buffer.isBuffer((req as any).rawBody)) throw new Error('raw body missing; HMAC cannot be verified');

Type guard

function hasRawBody(req: Request): req is Request & { rawBody: Buffer } {
  return Buffer.isBuffer((req as any).rawBody);
}

Prevention

When it happens

Trigger: App secret in the Node env differs from the Meta app's App Secret; (req as any).rawBody missing or reconstructed differently because express.json() consumed the stream without a verify callback; a proxy rewriting the body (recompression, re-serialization); signature computed over parsed JSON rather than the raw bytes.

Common situations: App secret rotated in the Meta console but not redeployed; express.json() mounted without capturing rawBody so the field is undefined or stale; different secrets across environments; whitespace-pasted secrets.

Related errors


AI-assisted analysis of sickn33/agentic-awesome-skills@58d857988f (2026-08-26). Data as JSON: /api/errors/9c280f0e6fb0ee3e. Report an issue: GitHub.