Immediately locking out a user on first payment failure destroys retention for what is often a recoverable situation — expired cards, temporary bank blocks, and 3DS challenges account for a large share of first-failure declines. Stripe's Smart Retries window is typically 4 attempts over 7–28 days. Without handling invoice.payment_failed, you either lose customers unnecessarily on first failure, or retain them indefinitely on final failure because you never processed the cancellation event. ISO 25010 fault-tolerance requires the system to degrade gracefully during partial failures.
Medium because the failure degrades revenue recovery and user experience significantly but does not create a security vulnerability or data loss.
Handle invoice.payment_failed to notify the user without revoking access immediately. Only revoke when Stripe moves the subscription to past_due or canceled after exhausting retries.
case 'invoice.payment_failed': {
const invoice = event.data.object
await db.user.update({
where: { stripe_customer_id: invoice.customer as string },
data: { payment_failed_at: new Date() }
})
// Trigger an email: "Your payment failed — please update your card"
// Do NOT revoke access here — Stripe will retry
break
}
Also handle customer.subscription.updated where status === 'past_due' to show an in-app banner, and revoke access only when status === 'canceled'. Enable Stripe Smart Retries and dunning emails in your Stripe Dashboard to handle the retry cadence automatically.
ID: saas-billing.subscription-mgmt.payment-failure-grace
Severity: medium
What to look for: Look for handling of Stripe's invoice.payment_failed or customer.subscription.updated events where status changes to past_due. Check whether the application handles the grace period between first payment failure and final subscription cancellation. Look for user notifications (email triggers, in-app banners) when payment fails. Examine whether the application immediately locks out users on first payment failure or allows continued access during Stripe's retry window.
Pass criteria: Count every payment failure notification channel. The application handles invoice.payment_failed (or equivalent) webhook events AND notifies users of payment failure through at least one channel: email (via Stripe email settings or custom webhook-triggered email), in-app notification/banner, or a visual indicator on the billing page showing payment status. Access continues during Stripe's configured retry window (typically 4 attempts over 7-28 days) and is revoked only when the subscription status moves to past_due → canceled.
Fail criteria: No handling of payment failure events — users are either permanently locked out on first failure or retain access after final cancellation because the event is never processed. Relying solely on Stripe Smart Retries without any user notification mechanism is also a FAIL. A grace period that silently retries without ever informing the user does not satisfy this check.
Skip (N/A) when: No payment integration or no subscription billing (one-time payments only do not have this lifecycle).
Detail on fail: "No handler for invoice.payment_failed — users are not notified of failed payments" or "customer.subscription.updated handler for past_due status immediately revokes access without allowing Stripe's retry window"
Remediation: Handle the payment failure lifecycle gracefully:
case 'invoice.payment_failed': {
const invoice = event.data.object
// Notify user their payment failed — send email, set in-app flag
await db.user.update({
where: { stripe_customer_id: invoice.customer as string },
data: { payment_failed_at: new Date() }
})
// Don't revoke access yet — Stripe will retry
// Access revokes when subscription status moves to 'past_due' or 'canceled'
break
}
Configure Stripe's automatic collection settings (Smart Retries + dunning emails) in your Stripe Dashboard to handle the retry cadence.