{"record":{"id":"542e437617f21532","repo":"sickn33/agentic-awesome-skills","slug":"webhook-verification-failed-invalid-token","errorCode":null,"errorMessage":"Webhook verification failed: invalid token","messagePattern":"Webhook verification failed: invalid token","errorType":"http","errorClass":null,"httpStatus":400,"severity":"warning","filePath":"skills/whatsapp-cloud-api/assets/boilerplate/nodejs/src/webhook-handler.ts","lineNumber":83,"sourceCode":"export function rawBodyMiddleware(req: Request, _res: Response, buf: Buffer): void {\n  (req as any).rawBody = buf;\n}\n\n/**\n * Handler de verificacao do webhook (GET).\n * A Meta envia um challenge que deve ser retornado para confirmar o endpoint.\n */\nexport function handleWebhookVerification(verifyToken: string) {\n  return (req: Request, res: Response): void => {\n    const mode = req.query['hub.mode'] as string;\n    const token = req.query['hub.verify_token'] as string;\n    const challenge = req.query['hub.challenge'] as string;\n\n    if (mode === 'subscribe' && token === verifyToken && SAFE_CHALLENGE_RE.test(challenge)) {\n      console.log('Webhook verified successfully');\n      res.type('text/plain').status(200).send(challenge);\n    } else {\n      console.warn('Webhook verification failed: invalid token');\n      res.status(mode === 'subscribe' && token === verifyToken ? 400 : 403).send();\n    }\n  };\n}\n\n/**\n * Extrai mensagens e status updates do payload do webhook.\n */\nexport function parseWebhookPayload(payload: WebhookPayload): {\n  messages: IncomingMessage[];\n  statuses: StatusUpdate[];\n} {\n  const messages: IncomingMessage[] = [];\n  const statuses: StatusUpdate[] = [];\n\n  for (const entry of payload.entry || []) {\n    for (const change of entry.changes || []) {\n      if (change.value.messages) {","sourceCodeStart":65,"sourceCodeEnd":101,"githubUrl":"https://github.com/sickn33/agentic-awesome-skills/blob/58d857988fcfac6986206bca2b2fe223aa437e4b/skills/whatsapp-cloud-api/assets/boilerplate/nodejs/src/webhook-handler.ts#L65-L101","documentation":"Logged by handleWebhookVerification when Meta's GET callback verification fails the check mode === 'subscribe' && token === verifyToken && SAFE_CHALLENGE_RE.test(challenge). The handler replies 403 for a bad token/mode and 400 when the token matches but hub.challenge fails the safety regex. It means the WhatsApp Cloud API webhook subscription handshake did not succeed.","triggerScenarios":"Meta sends GET /webhook?hub.mode=subscribe&hub.verify_token=...&hub.challenge=... when you click 'Verify and save' in the Meta App Dashboard. The warning fires when hub.verify_token differs from the server's VERIFY_TOKEN, when hub.mode is not 'subscribe', or when hub.challenge contains characters rejected by SAFE_CHALLENGE_RE (correct token but 400 response). Any unrelated GET to the webhook URL also triggers it (403).","commonSituations":"VERIFY_TOKEN env var on the server differs from the Verify Token typed into the Meta dashboard; trailing whitespace or quotes in the env var; a new deployment that lost the env var; a proxy or query parser mangling/encoding hub.challenge so SAFE_CHALLENGE_RE fails and a 400 is returned instead of echoing the challenge.","solutions":["Check the response status your handler returned: 403 means token/mode mismatch — fix the token; 400 means the token matched but hub.challenge failed SAFE_CHALLENGE_RE — inspect the incoming challenge for unexpected characters or encoding.","Make the server's VERIFY_TOKEN env var exactly equal to the Verify Token field in Meta App Dashboard > WhatsApp > Configuration.","Ensure VERIFY_TOKEN is set in the deployment environment with no quotes or whitespace (printf '%s' \"$VERIFY_TOKEN\" | wc -c to check).","Redeploy/restart the Node process after changing the env var, then retry verification in the Meta dashboard.","If a reverse proxy sits in front, confirm it forwards the query string untouched and does not double-encode hub.challenge."],"exampleFix":"// before: token read raw from env, may carry whitespace\nconst verifyToken = process.env.VERIFY_TOKEN;\n\n// after: trim and fail fast when unset\nconst verifyToken = (process.env.VERIFY_TOKEN ?? '').trim();\nif (!verifyToken) {\n  throw new Error('VERIFY_TOKEN env var is not set');\n}","handlingStrategy":"validation","validationCode":"// Reject malformed verification requests before the handler logic\nfunction isValidVerificationQuery(req: Request): boolean {\n  const mode = req.query['hub.mode'];\n  const token = req.query['hub.verify_token'];\n  const challenge = req.query['hub.challenge'];\n  return typeof mode === 'string' &&\n    typeof token === 'string' &&\n    typeof challenge === 'string' &&\n    challenge.length > 0 && challenge.length <= 256;\n}","typeGuard":"function isWebhookVerificationQuery(q: unknown): q is Record<'hub.mode' | 'hub.verify_token' | 'hub.challenge', string> {\n  if (typeof q !== 'object' || q === null) return false;\n  const r = q as Record<string, unknown>;\n  return typeof r['hub.mode'] === 'string' &&\n    typeof r['hub.verify_token'] === 'string' &&\n    typeof r['hub.challenge'] === 'string';\n}","tryCatchPattern":null,"preventionTips":["Set VERIFY_TOKEN from a CI-verified secret and fail startup when it is missing or empty, so drift is caught at deploy time rather than during Meta's verification call.","Log the token's length or hash (never the value) at startup to compare with what was entered in the Meta dashboard.","Unit-test handleWebhookVerification for matching token, wrong token, and regex-failing challenge to lock in the 200/403/400 contract.","Monitor 4xx rates on the GET webhook route; a spike after a deploy usually means an env/dashboard token mismatch."],"tags":["whatsapp","webhook","verification","token","nodejs","meta-cloud-api"],"backgroundTag":"webhook-verification-failed","analyzedSha":"58d857988fcfac6986206bca2b2fe223aa437e4b","analyzedAt":"2026-08-26T11:55:59.350Z","schemaVersion":2},"datasetVersion":"2026-08-26T14:46:13.012Z"}