yikart/AiToEarn · warning

-1

-1

Error message

ChannelWebhookInvalidSignature

What it means

The Douyin webhook provider rejects incoming webhook requests whose verify(request) check fails, responding 401 with code -1 and message ChannelWebhookInvalidSignature. The Douyin-signed payload did not match the configured secret, so the body is never parsed and no publish state is updated.

Source

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

import { PublishRecordRepository } from '@yikart/mongodb'
import { PublishStateService } from '../../publish/tasks/publish-state.service'
import { DouyinConfig } from './douyin.config'
import { buildDouyinVideoWorkLink, DouyinWebhookBodySchema, DouyinWebhookEvent } from './douyin.interface'

@Injectable()
export class DouyinWebhookProvider implements PlatformWebhookHandler {
  private readonly logger = new Logger(DouyinWebhookProvider.name)

  constructor(
    private readonly config: DouyinConfig,
    @Optional() private readonly publishRecordRepo?: PublishRecordRepository,
    @Optional() private readonly stateService?: PublishStateService,
  ) {}

  async handle(request: Request, response: Response): Promise<void> {
    if (!this.verify(request)) {
      this.logger.warn({ platform: AccountType.Douyin }, 'Douyin webhook signature invalid')
      response.status(401).json({ code: -1, message: getCodeMessage(ResponseCode.ChannelWebhookInvalidSignature, undefined, getLocale()) })
      return
    }

    const body = this.parseWebhookBody(request.body)
    if (!body) {
      response.status(200).json({ code: 0, message: 'ok' })
      return
    }

    if (body.event === DouyinWebhookEvent.VerifyWebhook) {
      response.status(200).json({ challenge: body.content.challenge })
      return
    }

    if (body.event === DouyinWebhookEvent.CreateVideo) {
      await this.applyCreateVideoResult(body)
      response.status(200).json({ code: 0, message: 'ok' })
      return

View on GitHub (pinned to d3aa8bea5b)

Solutions

  1. Verify the server's Douyin webhook secret matches the one configured in the Douyin open platform
  2. Ensure the raw request body is passed unchanged through any reverse proxy to the handler
  3. Confirm the request is hitting the correct environment deployment (cn vs intl secrets)
  4. Rotate and re-register the webhook secret on both sides if compromised or drifted

Example fix

// before
if (!this.verify(request)) {
  this.logger.warn({ platform: AccountType.Douyin }, 'Douyin webhook signature invalid')
// after
if (!this.verify(request)) {
  this.logger.warn({ platform: AccountType.Douyin, headers: request.headers['x-signature'] ? 'present' : 'missing' }, 'Douyin webhook signature invalid')
Defensive patterns

Strategy: validation

Validate before calling

const expected = crypto.createHmac('sha256', douyinSecret).update(rawBody).digest('hex')
if (req.headers['x-signature'] !== expected) return res.status(401).json({ code: -1 })

Type guard

function isDouyinSigned(req: Request, secret: string): boolean {
  const sig = req.headers['x-signature'] as string | undefined
  return !!sig && crypto.timingSafeEqual(Buffer.from(sig), crypto.createHmac('sha256', secret).update((req.body as Buffer)).digest())
}

Try / catch

app.post('/webhooks/douyin', express.raw({ type: 'application/json' }), (req, res) => {
  if (!isDouyinSigned(req, process.env.DOUYIN_WEBHOOK_SECRET!)) {
    return res.status(401).json({ code: -1, message: 'ChannelWebhookInvalidSignature' })
  }
  // parse and process payload
})

Prevention

When it happens

Trigger: POST to the Douyin webhook endpoint with a missing/incorrect signature header, a secret mismatch between Douyin's app config and the server env, or the body altered in transit before verification.

Common situations: Secret rotated in Douyin open-platform console without updating server config; gateway/proxy recompressing or re-encoding the payload; wrong environment deployment receiving the callback; attackers probing the endpoint.

Related errors


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