5 min read

How to Set Up Alerts for Failed Payments in Stripe

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.

javascript
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'));
Basic Express endpoint for Stripe webhooks. Run this on a public HTTPS server.

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.

javascript
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_SECRET
Register a webhook endpoint that listens for payment_intent.payment_failed events.

Or 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.

Watch out: Always validate the webhook signature before processing the event. Stripe signs every webhook with your secret, so you can be sure the request came from Stripe and not an attacker.

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.

javascript
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 });
});
stripe.webhooks.constructEvent() validates the signature and parses the event.

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.

javascript
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);
}
Extract the PaymentIntent details and send a Slack notification.

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.

Track these metrics automatically

Product Analyst connects to your stack and surfaces the insights that matter.

Try Product Analyst — Free