Renewal charges that arrive with no advance warning are the leading cause of subscription-related chargebacks and the behavioral trigger the FTC Negative Option Rule (2025) and California ARL target in their 'save offer' and notice requirements. A user who has forgotten they subscribed months ago and sees an unexpected $29 charge will dispute it with their bank rather than cancel — and Stripe's 0.75% dispute threshold affects payment processing eligibility. California ARL requires advance notice for annual charges; for monthly charges, renewal reminders are a strong chargeback-prevention measure even where not strictly mandated.
Low because monthly renewal reminders are a chargeback-prevention best practice and FTC good-faith signal rather than a per-violation enforcement target, but the absence of annual reminders specifically triggers California ARL exposure.
Handle Stripe's invoice.upcoming event and send a renewal reminder email. In Stripe Dashboard, set 'Days before upcoming invoice email' to 7 for monthly plans. In app/api/webhooks/stripe/route.ts:
if (event.type === 'invoice.upcoming') {
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 && invoice.amount_due > 0) {
await sendEmail({
to: email,
subject: `Heads up: Your subscription renews ${formatDate(invoice.period_end)}`,
template: 'renewal-reminder',
data: {
amount: (invoice.amount_due / 100).toFixed(2),
renewalDate: formatDate(invoice.period_end),
cancelUrl: `${process.env.NEXT_PUBLIC_APP_URL}/settings/billing`,
},
})
}
}
Email must include renewal amount, date, and a direct cancel link. The annual-renewal-reminder check covers the ≥30-day advance notice required for annual subscriptions under California ARL.
ID: subscription-compliance.renewal.renewal-reminder
Severity: low
What to look for: Check for a webhook handler for Stripe's invoice.upcoming event, which fires 3 days before a subscription renewal by default (configurable to up to 30 days in Stripe Dashboard). Does this webhook trigger an email to the user? Find the email template — does it state the upcoming renewal amount, the renewal date, and how to cancel before the charge? Also check Stripe Dashboard settings: Billing → Subscriptions → Smart Retries and email notifications — Stripe can send automatic renewal reminders, but custom reminders with cancellation instructions are preferable. For monthly subscriptions, advance notice is good UX but not always legally required. For annual subscriptions, California's ARL requires a reminder before the annual renewal charge — check the annual-renewal-reminder check for that specific requirement. Count every subscription tier in the application and enumerate which have automated renewal reminders configured vs. which lack them.
Pass criteria: A renewal reminder email is sent to users before each billing cycle. The reminder includes the renewal amount, the renewal date, and a direct path to cancel. The reminder arrives with sufficient advance notice for the user to act (at minimum 3 days; 7 days is better practice).
Fail criteria: No renewal reminder emails are sent. Stripe's invoice.upcoming event has no webhook handler. Users discover renewals only when they see the charge on their bank statement.
Skip (N/A) when: The application has no subscription or recurring billing.
Cross-reference: The price-change-notification check verifies consumers are notified when the renewal price changes from what they originally agreed to.
Detail on fail: Example: "No invoice.upcoming webhook handler found. No renewal reminder emails are sent before monthly or annual charges." or "Stripe's built-in dunning emails are enabled but no custom reminder with cancellation instructions is sent.".
Remediation: Configure Stripe to send the invoice.upcoming event earlier and handle it with a reminder email:
// Step 1: Configure invoice.upcoming timing in Stripe Dashboard
// Billing → Subscriptions → Manage → "Days before upcoming invoice email"
// Set to 7 days for monthly, 30 days for annual
// Step 2: Handle the webhook
// app/api/webhooks/stripe/route.ts
if (event.type === 'invoice.upcoming') {
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 && invoice.amount_due > 0) {
await sendEmail({
to: email,
subject: `Heads up: Your subscription renews ${formatDate(invoice.period_end)}`,
template: 'renewal-reminder',
data: {
amount: (invoice.amount_due / 100).toFixed(2),
currency: invoice.currency.toUpperCase(),
renewalDate: formatDate(invoice.period_end),
cancelUrl: `${process.env.NEXT_PUBLIC_APP_URL}/settings/billing`,
},
})
}
}
Email content: "Your [Plan Name] subscription will renew on [date] for $[amount]. If you'd like to cancel before then, visit [link] — cancellations take effect at the end of your current period."