sickn33/agentic-awesome-skills · warning
Webhook verification failed: invalid token
Error message
Webhook verification failed: invalid token
What it means
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.
Source
Thrown at skills/whatsapp-cloud-api/assets/boilerplate/nodejs/src/webhook-handler.ts:83
export function rawBodyMiddleware(req: Request, _res: Response, buf: Buffer): void {
(req as any).rawBody = buf;
}
/**
* Handler de verificacao do webhook (GET).
* A Meta envia um challenge que deve ser retornado para confirmar o endpoint.
*/
export function handleWebhookVerification(verifyToken: string) {
return (req: Request, res: Response): void => {
const mode = req.query['hub.mode'] as string;
const token = req.query['hub.verify_token'] as string;
const challenge = req.query['hub.challenge'] as string;
if (mode === 'subscribe' && token === verifyToken && SAFE_CHALLENGE_RE.test(challenge)) {
console.log('Webhook verified successfully');
res.type('text/plain').status(200).send(challenge);
} else {
console.warn('Webhook verification failed: invalid token');
res.status(mode === 'subscribe' && token === verifyToken ? 400 : 403).send();
}
};
}
/**
* Extrai mensagens e status updates do payload do webhook.
*/
export function parseWebhookPayload(payload: WebhookPayload): {
messages: IncomingMessage[];
statuses: StatusUpdate[];
} {
const messages: IncomingMessage[] = [];
const statuses: StatusUpdate[] = [];
for (const entry of payload.entry || []) {
for (const change of entry.changes || []) {
if (change.value.messages) {View on GitHub (pinned to 58d857988f)
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.
Example fix
// before: token read raw from env, may carry whitespace
const verifyToken = process.env.VERIFY_TOKEN;
// after: trim and fail fast when unset
const verifyToken = (process.env.VERIFY_TOKEN ?? '').trim();
if (!verifyToken) {
throw new Error('VERIFY_TOKEN env var is not set');
} Defensive patterns
Strategy: validation
Validate before calling
// Reject malformed verification requests before the handler logic
function isValidVerificationQuery(req: Request): boolean {
const mode = req.query['hub.mode'];
const token = req.query['hub.verify_token'];
const challenge = req.query['hub.challenge'];
return typeof mode === 'string' &&
typeof token === 'string' &&
typeof challenge === 'string' &&
challenge.length > 0 && challenge.length <= 256;
} Type guard
function isWebhookVerificationQuery(q: unknown): q is Record<'hub.mode' | 'hub.verify_token' | 'hub.challenge', string> {
if (typeof q !== 'object' || q === null) return false;
const r = q as Record<string, unknown>;
return typeof r['hub.mode'] === 'string' &&
typeof r['hub.verify_token'] === 'string' &&
typeof r['hub.challenge'] === 'string';
} Prevention
- 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.
When it happens
Trigger: 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).
Common situations: 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.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
AI-assisted analysis of sickn33/agentic-awesome-skills@58d857988f (2026-08-26).
Data as JSON: /api/errors/542e437617f21532.
Report an issue: GitHub.