Complete Guide: Listening to the Stripe invoice.payment_failed Webhook

As a developer integrating Stripe Billing into your SaaS, recovering failed charges is not magic. It means processing real-time events through Webhooks. Here is the definitive guide to handling your silent MRR leak.

Executive Summary (TL;DR)

  • The `invoice.payment_failed` event fires when a charge attempt for a recurring customer fails (expired card, insufficient funds, wrong CVC).
  • Always verify Webhook signatures (Stripe Signature) to prevent spoofing attacks in production.
  • Building a manual email flow requires a retry database and templates. Dunning LITE does it for $29/month.

What Is the Stripe invoice.payment_failed Event

Stripe does not automatically notify your customer when a payment fails (unless you configure their basic Smart Retries). They send a massive JSON payload to your server. That is where the invoice.payment_failed webhook comes in: it is the early warning signal for your dunning system.

invoice.payment_failed Payload Anatomy

The most important fields inside data.object are:

  • customer_email: Who to contact.
  • amount_due: How much is owed.
  • next_payment_attempt: When Stripe will retry.
  • charge.failure_code: The exact reason from the bank (insufficient_funds, expired_card).

Stripe Webhook Implementation in Node.js (Next.js App Router)

Webhook Signature Verification (Stripe Signature)

This is the minimum viable code to securely receive the invoice.payment_failed event, validating your endpoint secret key:

typescript (app/api/webhook/route.ts)
import { headers } from 'next/headers';
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: '2023-10-16' });
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET!;

export async function POST(req: Request) {
  const body = await req.text();
  const signature = headers().get('stripe-signature') as string;

  let event: Stripe.Event;

  try {
    event = stripe.webhooks.constructEvent(body, signature, endpointSecret);
  } catch (err: any) {
    return new Response(`Webhook Error: ${err.message}`, { status: 400 });
  }

  if (event.type === 'invoice.payment_failed') {
    const invoice = event.data.object as Stripe.Invoice;
    // TODO: Send "Update your payment method" email
    console.log(`🚨 Failed payment of ${invoice.amount_due / 100} for customer ${invoice.customer}`);
  }

  return new Response(JSON.stringify({ received: true }), { status: 200 });
}

The “I'll build it myself this weekend” trap

The code above is trivial. But the real Dunning system (the recovery layer) is complex. You need:

  • Responsive email templates that inject a secure static Checkout Session link.
  • Scheduled cron jobs if you want reminders on day 1, 3, and 7.
  • Logic to pause the flow if the customer pays on their own.
  • Analytics to see which emails get opened and which ones convert.

Building a side SaaS just to recover your main MRR distracts you from delivering value to your actual customers.

The 2-Minute Alternative: Dunning LITE Without Your Own Webhook

Forget the code. Forget maintaining SendGrid templates. Connect your account in one click using Stripe Connect. We listen to the webhook, analyze why it failed, and orchestrate a human email campaign on your behalf.

Create Your Free Dunning LITE Account

Join The Dunning Letter

Tactical SaaS strategies to reduce churn and recover revenue. 1 email per month.