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.

Solutions

  1. 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.
  2. Make the server's VERIFY_TOKEN env var exactly equal to the Verify Token field in Meta App Dashboard > WhatsApp > Configuration.
  3. Ensure VERIFY_TOKEN is set in the deployment environment with no quotes or whitespace (printf '%s' "$VERIFY_TOKEN" | wc -c to check).
  4. Redeploy/restart the Node process after changing the env var, then retry verification in the Meta dashboard.
  5. 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

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

Related errors


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

Appendix: 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)