hcengineering/platform · error

Invalid signature

Error message

Invalid signature

What it means

stripe.webhooks.constructEvent() threw while verifying the HMAC signature or parsing the payload, so the handler responds 403 'Invalid signature'. This covers signature mismatch, bad timestamp tolerance, and payload tampering.

Source

Thrown at services/payment/pod-payment/src/providers/stripe/webhook.ts:64

      return
    }

    if (sig === undefined) {
      ctx.error('Missing Stripe signature header')
      res.status(400).json({ error: 'Missing signature' })
      return
    }

    // Create Stripe instance for webhook verification
    const stripe = new Stripe(stripeApiKey, { apiVersion: '2025-02-24.acacia' })

    // Verify webhook signature and parse event
    let event: Stripe.Event
    try {
      event = stripe.webhooks.constructEvent(rawBody, sig, webhookSecret)
    } catch (err: any) {
      ctx.error('Invalid Stripe webhook signature', { err })
      res.status(403).json({ error: 'Invalid signature' })
      return
    }

    // Route to appropriate handler based on event type
    switch (event.type) {
      case 'customer.subscription.created':
      case 'customer.subscription.updated':
      case 'customer.subscription.deleted': {
        void handleSubscriptionUpdated(ctx, accountsUrl, serviceToken, event).catch((err) => {
          ctx.error('Failed to process Stripe webhook event', { event, err })
        })
        break
      }
      case 'invoice.payment_succeeded':
      case 'invoice.payment_failed': {
        void (async () => {
          try {
            const subscriptionEvent = await createSubscriptionEventFromInvoiceEvent(ctx, stripe, event)

View on GitHub (pinned to 63e28dc964)

Solutions

  1. Copy the exact whsec_... signing secret from the Stripe dashboard endpoint (or stripe listen output) into STRIPE_WEBHOOK_SECRET
  2. Ensure the raw Buffer body is passed unmodified to constructEvent
  3. Check server clock skew (NTP) if errors mention timestamp tolerance
  4. Confirm the secret belongs to the same endpoint (and mode: test vs live) that is sending events

Example fix

// before (wrong secret: API key)
STRIPE_WEBHOOK_SECRET=sk_test_...
// after
STRIPE_WEBHOOK_SECRET=whsec_abc123_from_endpoint_settings
Defensive patterns

Strategy: try-catch

Validate before calling

if (!process.env.STRIPE_WEBHOOK_SECRET?.startsWith('whsec_')) {
  throw new Error('STRIPE_WEBHOOK_SECRET must be the endpoint signing secret (whsec_...)')
}

Try / catch

try {
  const event = stripe.webhooks.constructEvent(rawBody, sig, webhookSecret)
} catch (err) {
  ctx.error('Invalid Stripe webhook signature', { err })
  res.status(403).json({ error: 'Invalid signature' })
  return
}

Prevention

When it happens

Trigger: STRIPE_WEBHOOK_SECRET doesn't match the signing secret of the endpoint (whsec_...); body re-serialized before verification (loses raw bytes); clock skew beyond tolerance; using the secret of a different endpoint (each Stripe endpoint has its own secret).

Common situations: Copy-pasting the account's restricted key instead of the webhook signing secret; mixing test/live mode endpoints; API gateway re-encoding bodies.

Related errors


AI-assisted analysis of hcengineering/platform@63e28dc964 (2026-08-29). Data as JSON: /api/errors/65bcdc86ceca15e8. Report an issue: GitHub.