When a subscription payment fails silently — no email, no in-app alert, no documented grace period — the consumer's access may be revoked without warning while they believe the subscription is active. FTC Negative Option Rule (2025) requires that consumers be informed of failed charges and given an opportunity to resolve payment before losing access. The absence of a invoice.payment_failed webhook handler also means Stripe's Smart Retry window passes without the user knowing they need to update their payment method, converting a recoverable payment failure into a churned subscriber and a potential service disruption dispute.
Info because failed payment notification is a consumer-protection obligation under FTC rules but a violation only materializes when a specific payment fails without notice, not as an ongoing structural defect.
Handle the invoice.payment_failed webhook and send an immediate notification with grace period disclosure. In app/api/webhooks/stripe/route.ts:
if (event.type === 'invoice.payment_failed') {
const invoice = event.data.object
const customer = await stripe.customers.retrieve(invoice.customer as string)
const email = 'email' in customer ? customer.email : null
if (email) {
await sendEmail({
to: email,
subject: 'Action required: Payment failed for your subscription',
template: 'payment-failed',
data: {
amount: (invoice.amount_due / 100).toFixed(2),
updatePaymentUrl: `${process.env.NEXT_PUBLIC_APP_URL}/settings/billing`,
graceEndDate: formatDate((invoice.period_end ?? 0) + 60 * 60 * 24 * 21),
},
})
}
}
Document the grace period in src/app/terms/page.tsx or the billing FAQ: 'If payment fails, Stripe will retry for up to 21 days. You retain full access during this period. After that, your subscription is cancelled and you move to the free tier.'
ID: subscription-compliance.renewal.failed-payment-handling
Severity: info
What to look for: Check the application's handling of failed subscription payments. Look for webhook handlers for invoice.payment_failed and customer.subscription.updated where status changes to past_due or unpaid. Does the application email the user when a payment fails? Does it describe the grace period (how long the user retains access before the subscription is cancelled)? Check Stripe's Smart Retries configuration (Billing → Subscriptions → Smart Retries) — Stripe retries failed payments automatically over several days, but the user needs to know this is happening and how long they have to update their payment method. Check the application's ToS or billing FAQ for a documented failed payment policy. Count all instances found and enumerate each.
Pass criteria: When a payment fails, the user is notified via email with: a description of why (payment failed), how long they have to update their payment method (grace period), and a link to update payment details. The grace period and failed payment behavior are documented in the ToS or billing FAQ. At least 1 implementation must be confirmed.
Fail criteria: No invoice.payment_failed webhook handler. Failed payments result in silent subscription cancellation with no advance warning. No documented grace period anywhere in the application.
Skip (N/A) when: The application has no subscription or recurring billing.
Cross-reference: The cancellation-confirmation check verifies the termination flow triggered when failed payment retries are exhausted.
Detail on fail: Example: "No invoice.payment_failed webhook handler. Failed payments are handled by Stripe Smart Retries but users receive no custom notification and the grace period is not disclosed." or "Failed payment email is sent by Stripe's Dunning feature, but no custom email with grace period information is sent from the application.".
Remediation: Handle payment failures with a clear user communication:
// app/api/webhooks/stripe/route.ts
if (event.type === 'invoice.payment_failed') {
const invoice = event.data.object
const customer = await stripe.customers.retrieve(invoice.customer as string)
const email = 'email' in customer ? customer.email : null
if (email) {
await sendEmail({
to: email,
subject: 'Action required: Payment failed for your subscription',
template: 'payment-failed',
data: {
amount: (invoice.amount_due / 100).toFixed(2),
updatePaymentUrl: `${process.env.NEXT_PUBLIC_APP_URL}/settings/billing`,
// Stripe retries over 4 attempts across ~3 weeks by default
graceEndDate: formatDate(
(invoice.period_end ?? 0) + 60 * 60 * 24 * 21 // 21-day default Stripe window
),
},
})
}
}
Also document the policy: "If your payment fails, we will retry over 21 days. You will retain full access during this time. If payment is not resolved, your subscription will be cancelled and you will move to the free tier."