FTC Negative Option Rule (2025) and California ARL both require written confirmation of subscription enrollment including material terms — price, billing period, next billing date, and cancellation instructions. Stripe's default payment receipt satisfies the payment acknowledgment but does not include a cancellation path, which is the regulatory requirement most commonly cited in enforcement guidance. The confirmation email also serves as the user's primary reference if they want to cancel before the next billing date; without it, support tickets and chargebacks increase measurably.
Medium because the absence of a compliant confirmation email violates written-notice requirements under FTC rules and state ARL statutes, though the enrollment itself may have been validly consented to.
Handle the checkout.session.completed Stripe webhook and send a custom confirmation email that includes all material terms. In app/api/webhooks/stripe/route.ts:
if (event.type === 'checkout.session.completed') {
const subscription = await stripe.subscriptions.retrieve(
session.subscription as string
)
await sendEmail({
to: session.customer_email!,
subject: 'Your subscription is confirmed',
template: 'subscription-confirmation',
data: {
planName: subscription.items.data[0]?.plan.nickname ?? 'Pro Plan',
price: (subscription.items.data[0]?.plan.amount ?? 0) / 100,
period: subscription.items.data[0]?.plan.interval ?? 'month',
nextBillingDate: formatDate(subscription.current_period_end),
cancelUrl: `${process.env.NEXT_PUBLIC_APP_URL}/settings/billing`,
},
})
}
The email template must include plan name, amount, billing frequency, next charge date, and a sentence: 'To cancel, visit [link] before [next billing date].'
ID: subscription-compliance.enrollment.purchase-confirmation
Severity: medium
What to look for: Check the application's email-sending logic for a subscription enrollment confirmation. Look for webhook handlers for checkout.session.completed (Stripe), subscription.created, or similar events that trigger a transactional email. Find the email template (search for files named subscription-confirmation, welcome, receipt, or look in your email templates directory). Read the template content: does it include the subscription price, the billing period, the next billing date, and instructions for how to cancel? Check whether Stripe's automatic receipt email is relied upon instead of a custom confirmation — Stripe receipts are good but may not include cancellation instructions. Also check whether the email is sent promptly (on webhook receipt) or if there is a delay that could leave the user without confirmation. Count all instances found and enumerate each.
Pass criteria: A confirmation email is sent immediately after successful enrollment. The email includes: the plan name, the price and billing period, the next billing date (or trial end date if applicable), and clear instructions for how to cancel (either a direct link to the cancellation flow or account settings). The email is sent from a recognizable sender address. At least 1 implementation must be confirmed.
Fail criteria: No confirmation email is sent upon enrollment — only Stripe's default receipt (which lacks cancellation instructions). Confirmation email exists but does not include the next billing date or cancellation path. Email is sent but only after a significant delay (hours rather than seconds).
Skip (N/A) when: The application has no subscription or recurring billing.
Detail on fail: Specify what is missing. Example: "No checkout.session.completed webhook handler that sends a custom confirmation email. Only Stripe's default payment receipt is sent, which contains no cancellation instructions." or "Confirmation email sent but does not include next billing date or cancellation link.".
Remediation: Handle the Stripe webhook and send a complete confirmation email:
// app/api/webhooks/stripe/route.ts
import { stripe } from '@/lib/stripe'
import { sendEmail } from '@/lib/email'
export async function POST(req: Request) {
const event = stripe.webhooks.constructEvent(
await req.text(),
req.headers.get('stripe-signature')!,
process.env.STRIPE_WEBHOOK_SECRET!
)
if (event.type === 'checkout.session.completed') {
const session = event.data.object
const subscription = await stripe.subscriptions.retrieve(
session.subscription as string
)
await sendEmail({
to: session.customer_email!,
subject: 'Your subscription is confirmed',
template: 'subscription-confirmation',
data: {
planName: subscription.items.data[0]?.plan.nickname ?? 'Pro Plan',
price: (subscription.items.data[0]?.plan.amount ?? 0) / 100,
currency: subscription.currency.toUpperCase(),
period: subscription.items.data[0]?.plan.interval ?? 'month',
nextBillingDate: new Date(subscription.current_period_end * 1000)
.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }),
cancelUrl: `${process.env.NEXT_PUBLIC_APP_URL}/settings/billing`,
},
})
}
return Response.json({ received: true })
}
The email template must include: plan name, amount, billing frequency, next charge date, and a sentence like "To cancel, visit [link] before [next billing date]."