hcengineering/platform · error · WebhookVerificationError
Invalid signature
Error message
Invalid signature
What it means
Polar's validateEvent() throws WebhookVerificationError when the webhook signature header does not match the HMAC computed from the raw body and the configured webhook secret. The handler responds 403 'Invalid signature' to reject untrusted requests.
Source
Thrown at services/payment/pod-payment/src/providers/polar/webhook.ts:72
case 'subscription.active':
case 'subscription.canceled':
case 'subscription.uncanceled':
case 'subscription.revoked':
void handleSubscriptionUpdated(ctx, accountsUrl, serviceToken, event).catch((err) => {
ctx.error('Failed to process Polar webhook event', { event, err })
})
break
default:
}
res.status(202).json({ received: true })
} catch (err) {
// Check if it's a validation error by class name
const isValidationError = err instanceof WebhookVerificationError
if (isValidationError) {
ctx.error('Invalid Polar webhook signature', { err })
res.status(403).json({ error: 'Invalid signature' })
return
}
ctx.error('Failed to process Polar webhook', { err })
res.status(500).json({ error: 'Internal server error' })
}
}
/**
* Handle subscription.created/updated/active
*/
async function handleSubscriptionUpdated (
ctx: MeasureContext,
accountsUrl: string,
serviceToken: string,
event: any
): Promise<void> {
const subscription = event.data ?? eventView on GitHub (pinned to 63e28dc964)
Solutions
- Verify the webhook secret env var matches the secret shown in the Polar dashboard for this endpoint exactly (no quotes/whitespace/newlines)
- Re-check that the raw Buffer body is untouched before validateEvent (no re-parsing/re-serialization)
- Rotate/recreate the webhook endpoint in Polar and copy the fresh secret into the service
- Confirm clock sanity and that the correct endpoint secret is used per environment
Example fix
// before (stale secret) POLAR_WEBHOOK_SECRET=whsec_old // after POLAR_WEBHOOK_SECRET=whsec_current_from_dashboard
Defensive patterns
Strategy: try-catch
Validate before calling
if (!process.env.POLAR_WEBHOOK_SECRET) {
throw new Error('POLAR_WEBHOOK_SECRET is not set')
} Try / catch
try {
const event = validateEvent(rawBody, req.headers as Record<string,string>, webhookSecret)
} catch (err) {
if (err instanceof WebhookVerificationError) {
res.status(403).json({ error: 'Invalid signature' })
return
}
throw err
} Prevention
- Keep the Polar dashboard secret and the env var in sync per environment
- Never log or commit the webhook secret
- Verify raw body bytes are unmodified before validateEvent
- Alert on 403 spikes (possible forgery or secret drift)
When it happens
Trigger: POLAR_WEBHOOK_SECRET env var differs from the secret configured in the Polar dashboard; an attacker or misconfigured client sends a forged request; the body was modified in transit (proxy re-encoding) so the HMAC no longer matches; multiple environments sharing one webhook endpoint with the wrong secret.
Common situations: Rotating the secret in Polar's dashboard without updating the service env; running staging against production webhook credentials; proxies that re-sign or mutate payloads.
Related errors
AI-assisted analysis of hcengineering/platform@63e28dc964 (2026-08-29).
Data as JSON: /api/errors/0f0e5df896989751.
Report an issue: GitHub.