Guides / Payments

How to Get Alerts for Failed Payment Recovery

3 min read·Payments

The payment recovery lifecycle

When a payment fails, Stripe retries automatically (dunning). You want to be notified at each stage:

  1. Initial failure → alert immediately
  2. Retry succeeded → log recovery
  3. All retries exhausted → urgent alert

Webhook handler

ts
// app/api/webhooks/stripe/route.ts (add to existing handler)
case "invoice.payment_action_required": {
  const invoice = event.data.object as Stripe.Invoice;
  await logit.now("dunning", {
    event: "Payment action required",
    description: `Customer ${invoice.customer_email ?? invoice.customer} needs to update card`,
    icon: "⚠️",
    notify: true,
    tags: { stage: "action_required" },
    metadata: {
      invoiceId: invoice.id,
      customerId: invoice.customer as string,
      email: invoice.customer_email,
      amount: (invoice.amount_due ?? 0) / 100,
    },
  });
  break;
}

case "invoice.payment_succeeded": {
  const invoice = event.data.object as Stripe.Invoice;
  // Check if this was a recovery (billing_reason = subscription_cycle means initial, not retry)
  if (invoice.billing_reason === "subscription_update" || invoice.attempt_count > 1) {
    await logit.now("dunning", {
      event: "Failed payment recovered",
      description: `$${((invoice.amount_paid ?? 0) / 100).toFixed(2)} recovered after ${invoice.attempt_count} attempts`,
      icon: "🎉",
      notify: true,
      tags: { stage: "recovered", attempts: String(invoice.attempt_count) },
      metadata: {
        invoiceId: invoice.id,
        customerId: invoice.customer as string,
        amount: (invoice.amount_paid ?? 0) / 100,
        attemptCount: invoice.attempt_count,
      },
    });
  }
  break;
}

case "customer.subscription.deleted": {
  const sub = event.data.object as Stripe.Subscription;
  // Check if deleted due to non-payment
  if (sub.cancellation_details?.reason === "payment_failed") {
    await logit.now("dunning", {
      event: "Subscription lost to dunning",
      description: `Customer ${sub.customer} churned — all retries exhausted`,
      icon: "🚨",
      notify: true,
      tags: { stage: "lost", reason: "payment_failed" },
      metadata: {
        subscriptionId: sub.id,
        customerId: sub.customer as string,
      },
    });
  }
  break;
}

Required webhook events

Add these to your Stripe webhook:

  • invoice.payment_failed
  • invoice.payment_action_required
  • invoice.payment_succeeded
  • customer.subscription.deleted

Use a dedicated dunning channel

Having a dedicated channel lets you see at a glance how many customers are in recovery at any time — and whether your dunning rate is improving.

Try LogIt free

7-day trial. No credit card required.

Start free