Your customer's payment fails at 2 AM and you don't find out until the next morning. By then, you've lost churn visibility and a customer upset by the failed charge. Stripe fires a payment_intent.payment_failed event every time a payment fails, but it doesn't email you by default—you need to wire up webhooks to stay in the loop.
Understand Payment Failure Events in Stripe
When a customer's payment fails, Stripe fires a webhook event. You'll intercept this event and trigger your alert logic.
Know the event type you're listening for
When a payment fails, Stripe sends a payment_intent.payment_failed event. This fires for declined cards, expired cards, insufficient funds, and any other failure reason. You'll use this event to trigger your alert logic. Check the Events section in your Stripe dashboard to see a live feed of these events in your account.
Set up an HTTPS endpoint to receive webhooks
Stripe can only send webhooks to public HTTPS endpoints. Create a simple endpoint in your backend that accepts POST requests. The endpoint doesn't need to be fancy—it just needs to receive JSON from Stripe, validate it, and process the event.
const express = require('express');
const app = express();
app.post('/stripe-webhook', express.raw({type: 'application/json'}), (req, res) => {
console.log('Webhook received:', req.body);
res.json({ received: true });
});
app.listen(3000, () => console.log('Listening on port 3000'));Register Your Webhook Endpoint with Stripe
Create the webhook endpoint via the Stripe API
Use the Stripe Node SDK to register your endpoint. Pass the endpoint URL and the event types you want to listen for. Stripe will return a signing secret—store this safely in an environment variable.
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const endpoint = await stripe.webhookEndpoints.create({
url: 'https://example.com/stripe-webhook',
enabled_events: ['payment_intent.payment_failed'],
});
console.log('Webhook signing secret:', endpoint.secret);
// Save endpoint.secret to your .env file as STRIPE_WEBHOOK_SECRETOr use the Stripe Dashboard for a quicker setup
Go to Developers > Webhooks in your Stripe dashboard. Click Add endpoint and paste your URL. Select payment_intent.payment_failed under Events to send. Stripe will generate a signing secret—copy it to your .env file.
Validate Signatures and Send Alerts
Verify the webhook signature before trusting the data
Stripe signs every webhook with your endpoint's secret. Before processing any event, construct the event using stripe.webhooks.constructEvent(). If the signature is invalid, Stripe will throw an error—reject the request.
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
app.post('/stripe-webhook', express.raw({type: 'application/json'}), (req, res) => {
const sig = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
console.error('Signature verification failed:', err.message);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
res.json({ received: true });
});Extract payment details and trigger the alert
Inside your webhook handler, check if the event type is payment_intent.payment_failed. Extract the PaymentIntent object from event.data.object. Get the customer ID, amount, and failure reason, then send an alert to your team via email, Slack, or SMS.
if (event.type === 'payment_intent.payment_failed') {
const paymentIntent = event.data.object;
const alert = {
customer: paymentIntent.customer,
amount: paymentIntent.amount_received / 100,
currency: paymentIntent.currency,
failure_reason: paymentIntent.last_payment_error?.message,
created: new Date(paymentIntent.created * 1000),
};
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: 'POST',
body: JSON.stringify({
text: `Payment failed: ${alert.customer} - ${alert.amount} ${alert.currency}. Reason: ${alert.failure_reason}`,
}),
});
console.log('Alert sent for failed payment:', alert);
}Log failures for monitoring and debugging
Store failed payments in a database or log service so you can track patterns (e.g., which payment methods fail most often, which times of day see the most failures). This helps you spot issues before they become critical.
Common Pitfalls
- Forgetting to validate webhook signatures—if you skip signature verification, an attacker can spoof fake payment failure events to trigger false alerts.
- Using HTTP instead of HTTPS—Stripe only sends webhooks to endpoints with valid HTTPS certificates. Localhost won't work for testing; use ngrok or a similar tunnel to expose your local server.
- Not handling duplicate events—Stripe may retry failed webhooks, so your endpoint could receive the same event twice. Log event IDs and deduplicate them in your handler.
- Storing the signing secret in code—keep your STRIPE_WEBHOOK_SECRET in environment variables, never hardcoded.
Wrapping Up
You now have a real-time alert system for failed payments in Stripe. When a payment fails, your team gets notified immediately, so you can reach out to customers and reduce churn. If you want to track failed payments automatically across all your payment tools and identify patterns at scale, Product Analyst can help.