yikart/AiToEarn · error

ChannelWebhookInvalidVerifyToken

Error message

ChannelWebhookInvalidVerifyToken

What it means

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.

Source

Thrown at project/aitoearn-backend/apps/aitoearn-server/src/core/channels/platforms/youtube/youtube-webhook.provider.ts:71

    response.status(204).send()
  }

  private handleChallenge(request: Request, response: Response): void {
    const mode = this.getQuery(request, 'hub.mode')
    const challenge = this.getQuery(request, 'hub.challenge')
    const verifyToken = this.getQuery(request, 'hub.verify_token')

    if (
      mode
      && challenge
      && (!this.config.webhookVerifyToken || verifyToken === this.config.webhookVerifyToken)
    ) {
      response.status(200).send(challenge)
      return
    }

    this.logger.warn({ platform: AccountType.YouTube, mode }, 'YouTube webhook challenge rejected')
    response.status(403).send(getCodeMessage(ResponseCode.ChannelWebhookInvalidVerifyToken, undefined, getLocale()))
  }

  private parseYoutubeNotification(request: RawBodyRequest): YoutubeVideoNotification {
    const xml = request.rawBody?.toString('utf8') ?? (typeof request.body === 'string' ? request.body : '')
    const raw = this.xmlParser.parse(xml) as YoutubeAtomFeed
    const entry = raw.feed?.entry
    const videoId = entry?.['yt:videoId'] ?? entry?.id?.replace(/^yt:video:/, '')

    return {
      videoId,
      permalink: this.pickYoutubeLink(entry?.link) ?? (videoId ? `https://www.youtube.com/watch?v=${videoId}` : undefined),
      raw,
    }
  }

  private async applyYoutubeVideoNotification(notification: YoutubeVideoNotification): Promise<void> {
    if (!notification.videoId || !this.publishRecordRepo || !this.stateService) {
      this.logger.warn({ platform: AccountType.YouTube, platformWorkId: notification.videoId }, 'YouTube webhook event cannot be matched')

View on GitHub (pinned to d3aa8bea5b)

Solutions

  1. Re-register the WebSub subscription with the verify token currently configured on the server
  2. Make the configured webhookVerifyToken match the token used at subscribe time and keep it stable across deploys
  3. Confirm the callback URL is publicly reachable and unchanged
  4. Persist subscriptions so renewals use the same token

Example fix

// before
// token rotated on server; old subscriptions still use old token
// after
// re-subscribe: POST to hub with hub.verify_token=<current configured token>
Defensive patterns

Strategy: validation

Validate before calling

const { 'hub.mode': mode, 'hub.verify_token': token, 'hub.challenge': challenge } = req.query as Record<string, string>
if (mode === 'subscribe' && token !== process.env.YOUTUBE_VERIFY_TOKEN) throw new Error('WebSub verify token mismatch')

Type guard

function isWebSubChallenge(q: Record<string, unknown>): q is { 'hub.mode': string; 'hub.verify_token': string; 'hub.challenge': string } {
  return q['hub.mode'] === 'subscribe' && typeof q['hub.verify_token'] === 'string' && typeof q['hub.challenge'] === 'string'
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of yikart/AiToEarn@d3aa8bea5b (2026-08-31). Data as JSON: /api/errors/9bcb3efb065d4434. Report an issue: GitHub.