{"record":{"id":"9bcb3efb065d4434","repo":"yikart/AiToEarn","slug":"channelwebhookinvalidverifytoken-9bcb3e","errorCode":null,"errorMessage":"ChannelWebhookInvalidVerifyToken","messagePattern":"ChannelWebhookInvalidVerifyToken","errorType":"http","errorClass":null,"httpStatus":403,"severity":"error","filePath":"project/aitoearn-backend/apps/aitoearn-server/src/core/channels/platforms/youtube/youtube-webhook.provider.ts","lineNumber":71,"sourceCode":"    response.status(204).send()\n  }\n\n  private handleChallenge(request: Request, response: Response): void {\n    const mode = this.getQuery(request, 'hub.mode')\n    const challenge = this.getQuery(request, 'hub.challenge')\n    const verifyToken = this.getQuery(request, 'hub.verify_token')\n\n    if (\n      mode\n      && challenge\n      && (!this.config.webhookVerifyToken || verifyToken === this.config.webhookVerifyToken)\n    ) {\n      response.status(200).send(challenge)\n      return\n    }\n\n    this.logger.warn({ platform: AccountType.YouTube, mode }, 'YouTube webhook challenge rejected')\n    response.status(403).send(getCodeMessage(ResponseCode.ChannelWebhookInvalidVerifyToken, undefined, getLocale()))\n  }\n\n  private parseYoutubeNotification(request: RawBodyRequest): YoutubeVideoNotification {\n    const xml = request.rawBody?.toString('utf8') ?? (typeof request.body === 'string' ? request.body : '')\n    const raw = this.xmlParser.parse(xml) as YoutubeAtomFeed\n    const entry = raw.feed?.entry\n    const videoId = entry?.['yt:videoId'] ?? entry?.id?.replace(/^yt:video:/, '')\n\n    return {\n      videoId,\n      permalink: this.pickYoutubeLink(entry?.link) ?? (videoId ? `https://www.youtube.com/watch?v=${videoId}` : undefined),\n      raw,\n    }\n  }\n\n  private async applyYoutubeVideoNotification(notification: YoutubeVideoNotification): Promise<void> {\n    if (!notification.videoId || !this.publishRecordRepo || !this.stateService) {\n      this.logger.warn({ platform: AccountType.YouTube, platformWorkId: notification.videoId }, 'YouTube webhook event cannot be matched')","sourceCodeStart":53,"sourceCodeEnd":89,"githubUrl":"https://github.com/yikart/AiToEarn/blob/d3aa8bea5b146a8675607cf0144d891aad3e9683/project/aitoearn-backend/apps/aitoearn-server/src/core/channels/platforms/youtube/youtube-webhook.provider.ts#L53-L89","documentation":"YouTube (WebSub) webhook provider validates hub.mode=subscribe challenges by comparing hub.verify_token to config.webhookVerifyToken. On mismatch it responds 403 with ChannelWebhookInvalidVerifyToken, and Google's WebSub hub treats the subscription as failed.","triggerScenarios":"GET WebSub subscription request where hub.verify_token is absent or differs from the configured token, during channel subscription or renewal via the YouTube hub.","commonSituations":"Verify token changed on the server while Google retries with the old token; subscription callback URL registered with a different token; token lost across deployments/environments.","solutions":["Re-register the WebSub subscription with the verify token currently configured on the server","Make the configured webhookVerifyToken match the token used at subscribe time and keep it stable across deploys","Confirm the callback URL is publicly reachable and unchanged","Persist subscriptions so renewals use the same token"],"exampleFix":"// before\n// token rotated on server; old subscriptions still use old token\n// after\n// re-subscribe: POST to hub with hub.verify_token=<current configured token>","handlingStrategy":"validation","validationCode":"const { 'hub.mode': mode, 'hub.verify_token': token, 'hub.challenge': challenge } = req.query as Record<string, string>\nif (mode === 'subscribe' && token !== process.env.YOUTUBE_VERIFY_TOKEN) throw new Error('WebSub verify token mismatch')","typeGuard":"function isWebSubChallenge(q: Record<string, unknown>): q is { 'hub.mode': string; 'hub.verify_token': string; 'hub.challenge': string } {\n  return q['hub.mode'] === 'subscribe' && typeof q['hub.verify_token'] === 'string' && typeof q['hub.challenge'] === 'string'\n}","tryCatchPattern":null,"preventionTips":["Keep the verify token constant for the life of subscriptions","Re-subscribe after changing the token","Confirm callback URL is stable and publicly reachable"],"tags":["webhook","verify-token","youtube","websub"],"backgroundTag":"webhook-verify-token-mismatch","analyzedSha":"d3aa8bea5b146a8675607cf0144d891aad3e9683","analyzedAt":"2026-08-31T14:19:24.185Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}