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 throughcustomer.subscription.deleted- a subscription was cancelledcharge.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 knowcustomer.subscription.updated- plan changes not reflected in your app
Low priority, log but don’t alert:
payment_intent.created- informational, not actionablecustomer.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.