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 ?? event

View on GitHub (pinned to 63e28dc964)

Solutions

  1. Verify the webhook secret env var matches the secret shown in the Polar dashboard for this endpoint exactly (no quotes/whitespace/newlines)
  2. Re-check that the raw Buffer body is untouched before validateEvent (no re-parsing/re-serialization)
  3. Rotate/recreate the webhook endpoint in Polar and copy the fresh secret into the service
  4. 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

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.