California Automatic Renewal Law (ARL) Business & Professions Code §17601 specifically requires that for annual subscriptions, customers receive an advance renewal notice before the charge — with sufficient lead time to cancel. A 3-day notice adequate for monthly charges does not satisfy the ARL for a $290 annual charge. EU Consumer Rights Directive 2011/83/EU similarly requires material pre-contractual information be provided for automatic contract extensions. Annual subscribers who receive a $290 charge without a prior reminder represent the highest-dollar, highest-dispute-rate segment of subscription chargebacks.
Info because non-compliance requires an annual renewal to occur without the mandated notice — but when it does occur, the California ARL civil penalty and chargeback exposure are disproportionate to the severity label.
Differentiate annual subscriptions in the invoice.upcoming webhook handler and send a 30-day-advance reminder. In app/api/webhooks/stripe/route.ts:
if (event.type === 'invoice.upcoming') {
const subscription = await stripe.subscriptions.retrieve(invoice.subscription as string)
const isAnnual = subscription.items.data[0]?.plan.interval === 'year'
const daysUntilRenewal = Math.round(
(invoice.period_end - Date.now() / 1000) / (60 * 60 * 24)
)
if (isAnnual && daysUntilRenewal >= 28 && email) {
await sendEmail({
to: email,
subject: `Your annual subscription renews in ${daysUntilRenewal} days`,
template: 'annual-renewal-reminder',
data: {
amount: (invoice.amount_due / 100).toFixed(2),
renewalDate: formatDate(invoice.period_end),
cancelDeadline: formatDate(invoice.period_end - 60 * 60 * 24 * 2),
cancelUrl: `${process.env.NEXT_PUBLIC_APP_URL}/settings/billing`,
},
})
}
}
In Stripe Dashboard → Billing → Customer portal, set upcoming invoice notification to 30 days for annual price IDs. Email must include renewal amount, renewal date, and 'To avoid being charged, cancel before [date] at [direct link].'
ID: subscription-compliance.renewal.annual-renewal-reminder
Severity: info
What to look for: Check specifically for handling of annual subscription renewals. The California Automatic Renewal Law (ARL) and similar laws in other states require that for annual (or longer) subscriptions, customers receive a reminder notice before the renewal date. Look for: (1) a webhook handler or cron job that targets invoice.upcoming events specifically for annual subscriptions, (2) a dedicated annual renewal reminder email template (distinct from the monthly reminder, with more prominent cancellation instructions), (3) timing — the reminder should be sent at least 30 days before the annual renewal (California ARL requires reasonable advance notice, typically interpreted as 30+ days). If using Stripe's built-in email notifications, check whether annual subscriptions are configured to receive upcoming invoice emails with sufficient lead time. Check the Stripe Dashboard: Billing → Subscriptions → Upcoming invoice notifications — verify that annual subscriptions have a longer advance notice period configured than monthly subscriptions. Count all instances found and enumerate each.
Pass criteria: Annual subscription customers receive a renewal reminder at least 30 days before their annual renewal date. The reminder includes the renewal amount (which may have changed since the prior year), the renewal date, and prominent cancellation instructions with a direct link.
Fail criteria: Annual subscribers receive the same 3-day-advance renewal reminder as monthly subscribers (insufficient notice for an annual charge). No annual-specific reminder is configured. Annual subscriptions renew with no advance notice whatsoever.
Skip (N/A) when: The application offers no annual subscription plan (only monthly billing).
Detail on fail: Example: "invoice.upcoming webhook is configured but uses the same 3-day notice for both monthly and annual subscriptions. California ARL requires more advance notice for annual renewals." or "No annual subscription plan exists — check is not applicable.".
Remediation: Configure a longer advance notice for annual subscriptions and customize the email:
// Configure in Stripe Dashboard:
// Billing → Subscriptions → Customer emails → Upcoming renewal
// Set "Number of days before renewal" to 30 for annual price IDs
// In webhook handler, differentiate monthly vs. annual:
if (event.type === 'invoice.upcoming') {
const invoice = event.data.object
const subscription = await stripe.subscriptions.retrieve(
invoice.subscription as string
)
const isAnnual = subscription.items.data[0]?.plan.interval === 'year'
const daysUntilRenewal = Math.round(
(invoice.period_end - Date.now() / 1000) / (60 * 60 * 24)
)
// Send annual reminder at 30+ days; skip if the generic webhook already sent at 7 days
if (isAnnual && daysUntilRenewal >= 28) {
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: `Your annual subscription renews in ${daysUntilRenewal} days`,
template: 'annual-renewal-reminder',
data: {
amount: (invoice.amount_due / 100).toFixed(2),
renewalDate: formatDate(invoice.period_end),
cancelDeadline: formatDate(invoice.period_end - 60 * 60 * 24 * 2),
cancelUrl: `${process.env.NEXT_PUBLIC_APP_URL}/settings/billing`,
},
})
}
}
}
Annual reminder email content must include: your subscription renews on [date] for $[amount], what is included in the plan, and "To avoid being charged, cancel before [date - 2 days] at [direct link]."