stripewebhooksnodetutorial

Alert on Failed Stripe Webhooks in Node.js

February 26, 2026

Stripe webhooks are how your app knows about payments, subscriptions, refunds, and disputes. When they work, everything flows. When they fail, you don’t get an error. The event just doesn’t arrive, and your system silently falls out of sync.

Maybe a customer paid but their account wasn’t upgraded. Maybe a subscription was cancelled but your database still shows it as active. You won’t find out until someone complains or you manually check Stripe’s dashboard.

Here’s how to catch these failures in your Node.js backend and get alerted immediately.

How Stripe webhooks fail

Stripe sends webhook events to your endpoint via HTTP POST. If your endpoint returns a non-2xx status code, Stripe retries the event. But several things can go wrong:

  • Your server was down when the event arrived
  • Your handler threw an unhandled exception
  • Signature verification failed due to a misconfigured secret
  • Your handler succeeded partially. It processed the payment but crashed before updating the database

Stripe retries failed deliveries for up to 72 hours, but if your handler keeps failing, the events are eventually dropped. And you’re never notified that this happened.

Setting up webhook handling with Express

A basic Stripe webhook handler in Express:

import express from 'express'
import Stripe from 'stripe'

const app = express()
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY)
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET

app.post('/webhooks/stripe', express.raw({ type: 'application/json' }), async (req, res) => {
  let event

  try {
    event = stripe.webhooks.constructEvent(req.body, req.headers['stripe-signature'], webhookSecret)
  } catch (err) {
    console.error('Webhook signature verification failed:', err.message)
    return res.status(400).send('Invalid signature')
  }

  try {
    await handleStripeEvent(event)
    res.status(200).json({ received: true })
  } catch (err) {
    console.error('Webhook handler failed:', err.message)
    res.status(500).send('Handler error')
  }
})

This works, but when handleStripeEvent throws, the only evidence is a log line. If you’re not watching logs in real time, you’ll miss it.

Adding failure alerts

Wrap your handler to send an alert when something goes wrong:

import { ApiAlerts } from 'apialerts-js'

const alerts = new ApiAlerts(process.env.API_ALERTS_KEY)

app.post('/webhooks/stripe', express.raw({ type: 'application/json' }), async (req, res) => {
  let event

  try {
    event = stripe.webhooks.constructEvent(req.body, req.headers['stripe-signature'], webhookSecret)
  } catch (err) {
    await alerts.send({
      message: `Stripe signature verification failed: ${err.message}`,
      channel: 'payments',
      tags: ['stripe', 'webhook', 'error'],
    })
    return res.status(400).send('Invalid signature')
  }

  try {
    await handleStripeEvent(event)
    res.status(200).json({ received: true })
  } catch (err) {
    await alerts.send({
      message: `Stripe webhook failed: ${event.type} - ${err.message}`,
      channel: 'payments',
      link: `https://dashboard.stripe.com/events/${event.id}`,
      tags: ['stripe', 'webhook', 'error'],
    })
    res.status(500).send('Handler error')
  }
})

Now you get an alert the moment something fails. The link field includes a direct URL to the event in your Stripe dashboard so you can inspect it immediately.

What events to watch closely

Not all webhook failures are equally urgent. These are the ones worth alerting on immediately:

Critical, alert and investigate now:

  • invoice.payment_failed - a customer’s payment didn’t go through
  • customer.subscription.deleted - a subscription was cancelled
  • charge.dispute.created - a chargeback was filed

Important, alert during business hours:

  • checkout.session.completed - if this fails, a customer paid but your system doesn’t know
  • customer.subscription.updated - plan changes not reflected in your app

Low priority, log but don’t alert:

  • payment_intent.created - informational, not actionable
  • customer.updated - metadata changes

You can filter which events trigger alerts in your handler:

const criticalEvents = [
  'invoice.payment_failed',
  'customer.subscription.deleted',
  'charge.dispute.created',
  'checkout.session.completed',
]

async function handleStripeEvent(event) {
  switch (event.type) {
    case 'invoice.payment_failed':
      await handlePaymentFailed(event.data.object)
      break
    case 'checkout.session.completed':
      await handleCheckoutComplete(event.data.object)
      break
    // ... other handlers
  }
}

Alerting on signature failures separately

Signature verification failures are different from handler errors. They usually mean:

  • Your webhook secret is wrong or rotated
  • Someone is sending fake events to your endpoint
  • Stripe changed something on their end

These deserve a distinct alert because they affect all events, not just one:

await alerts.send({
  message: 'Stripe webhook signature verification is failing. All events are being rejected',
  channel: 'payments',
  tags: ['stripe', 'security', 'urgent'],
})

If you start getting these repeatedly, check your STRIPE_WEBHOOK_SECRET environment variable against the value in your Stripe dashboard.

Going further with alerting

Once you have failure alerts in place, you can also add positive confirmations for critical flows:

case 'checkout.session.completed':
  await handleCheckoutComplete(event.data.object)
  await alerts.send({
    message: `New payment: ${event.data.object.amount_total / 100} ${event.data.object.currency.toUpperCase()}`,
    channel: 'payments',
    tags: ['stripe', 'sale'],
  })
  break

This gives you real-time visibility into revenue without opening the Stripe dashboard.

Next steps