yikart/AiToEarn · warning
ChannelWebhookChallengeCodeMissing
Error message
ChannelWebhookChallengeCodeMissing
What it means
LinkedIn's challenge handshake sends a challengeCode query parameter; the provider must answer with an HMAC-SHA256 (hex) of that code. If challengeCode is absent the provider cannot build the response and returns 400 with ChannelWebhookChallengeCodeMissing.
Source
Thrown at project/aitoearn-backend/apps/aitoearn-server/src/core/channels/platforms/linkedin/linkedin-webhook.provider.ts:41
return
}
if (!this.verify(request)) {
this.logger.warn({ platform: AccountType.LinkedIn }, 'LinkedIn webhook signature invalid')
response.status(401).send(getCodeMessage(ResponseCode.ChannelWebhookInvalidSignature, undefined, getLocale()))
return
}
const payload: LinkedInWebhookPayload = LinkedInWebhookPayloadSchema.parse(request.body)
this.logger.log({
platform: AccountType.LinkedIn,
eventCount: payload.events?.length ?? 0,
}, 'LinkedIn webhook events received')
response.status(200).json({ status: 'ok' })
}
private handleChallenge(request: Request, response: Response): void {
const query = request.query as LinkedInWebhookChallengeQuery
if (!query.challengeCode) {
response.status(400).send(getCodeMessage(ResponseCode.ChannelWebhookChallengeCodeMissing, undefined, getLocale()))
return
}
const secret = this.config.webhookSecret || this.config.clientSecret
const challengeResponse = createHmac('sha256', secret).update(query.challengeCode).digest('hex')
response.status(200).json({ challengeCode: query.challengeCode, challengeResponse })
}
private verify(request: RawBodyRequest): boolean {
const rawBody = request.rawBody
const signature = this.getHeader(request, 'x-li-signature')
const secret = this.config.webhookSecret || this.config.clientSecret
if (!signature || !rawBody || !secret) {
return false
}
const digest = createHmac('sha256', secret).update(rawBody).digest('hex')
const expected = signature.startsWith('sha256=') ? `sha256=${digest}` : digestView on GitHub (pinned to d3aa8bea5b)
Solutions
- Register the exact webhook URL LinkedIn documents so the challenge flow includes challengeCode
- Exclude the webhook GET endpoint from synthetic health checks or give monitors a dedicated health route
- Test the handshake by replicating LinkedIn's documented GET with challengeCode set
Example fix
// before curl https://api.example.com/webhooks/linkedin // after curl "https://api.example.com/webhooks/linkedin?challengeCode=abc123"
Defensive patterns
Strategy: validation
Validate before calling
const url = new URL(callbackUrl)
if (!url.searchParams.get('challengeCode')) {
console.warn('LinkedIn challenge handshake requires challengeCode query param')
} Type guard
function hasChallengeCode(q: unknown): q is { challengeCode: string } {
return typeof (q as any)?.challengeCode === 'string' && (q as any).challengeCode.length > 0
} Prevention
- Don't point uptime monitors at the challenge endpoint
- Register exactly the documented callback URL format
- Simulate the full handshake before going live
When it happens
Trigger: GET request to the LinkedIn webhook endpoint without a challengeCode query parameter (e.g., health checks, browsers, or misconfigured subscription URLs).
Common situations: Uptime monitors pinging the webhook GET URL; LinkedIn configured with a wrong callback URL that omits the challenge flow; manual URL testing without query params.
Related errors
- ChannelWebhookInvalidSignature
- InvalidAiTaskId
- InvalidAiTaskId
- ChannelAuthRefreshTokenMissing
- ChannelWebhookNotSupported
AI-assisted analysis of yikart/AiToEarn@d3aa8bea5b (2026-08-31).
Data as JSON: /api/errors/33e6611ec7ac6aae.
Report an issue: GitHub.