# Written cancellation confirmation sent immediately

- **Pattern:** `ab-002555` (`subscription-compliance.cancellation.cancellation-confirmation`)
- **Severity:** high
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002555
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002555)

## Why it matters

FTC Click-to-Cancel Rule (2024) requires that a cancellation confirmation be provided to the consumer immediately after cancellation — not only as a UI toast that disappears, but as a durable written record. California ARL and EU Consumer Rights Directive 2011/83/EU Article 12 both require confirmation of contract termination including the date through which access continues. Stripe's Billing Portal handles the portal UI but sends no custom cancellation email by default; the application must handle `customer.subscription.updated` (cancel_at_period_end) and `customer.subscription.deleted` webhooks to satisfy this requirement.

## Severity rationale

High because absent written cancellation confirmation violates durable-medium requirements under the FTC Click-to-Cancel Rule and EU Consumer Rights Directive, leaving the business unable to demonstrate consumer received confirmation.

## Remediation

Handle the `customer.subscription.updated` webhook to detect `cancel_at_period_end: true` and send a confirmation email immediately. In `app/api/webhooks/stripe/route.ts`:

```ts
if (event.type === 'customer.subscription.updated') {
  const subscription = event.data.object
  if (subscription.cancel_at_period_end) {
    const customer = await stripe.customers.retrieve(subscription.customer as string)
    const email = 'email' in customer ? customer.email : null
    if (email) {
      await sendEmail({
        to: email,
        subject: 'Your subscription has been cancelled',
        template: 'cancellation-confirmation',
        data: {
          accessUntil: formatDate(subscription.current_period_end),
          cancelUrl: `${process.env.NEXT_PUBLIC_APP_URL}/settings/billing`,
        },
      })
    }
  }
}
```

Email text must include: 'Your subscription has been cancelled. You will retain access until [date]. No further charges will be made.'

## Detection

- **ID:** `cancellation-confirmation`
- **Severity:** `high`
- **What to look for:** Find the webhook handler for `customer.subscription.deleted` or `customer.subscription.updated` (when `cancel_at_period_end` becomes `true`) in Stripe's event system. Check whether a cancellation confirmation email is sent in response to this event. Inspect the email template: does it confirm that the cancellation was received, state the date through which the subscription remains active (if the user cancelled mid-period), and confirm that no further charges will occur? If using Stripe Billing Portal for cancellation, note that Stripe does not automatically send a custom cancellation email — you must handle the webhook yourself. If the application has its own cancellation flow, check the POST handler for a transactional email send after the cancellation is recorded. Count every step in the cancellation flow and enumerate: confirmation screen, email confirmation, effective date disclosure, refund information.
- **Pass criteria:** A written cancellation confirmation is sent to the user's email address immediately (within seconds to minutes) after cancellation. The confirmation states: that the subscription has been cancelled, the date through which access continues (if different from the cancellation date), and that no further charges will be made. At least 1 implementation must be confirmed.
- **Fail criteria:** No cancellation confirmation email is sent. Stripe Billing Portal handles cancellation but no webhook handler sends a custom confirmation email. Confirmation is only shown in the UI (a success toast) without a written record sent to the user's email.
- **Skip (N/A) when:** The application has no subscription or recurring billing.
- **Detail on fail:** Example: `"No customer.subscription.deleted webhook handler found. No cancellation confirmation email is sent."` or `"customer.subscription.updated webhook handler exists but does not check for cancel_at_period_end and does not send a cancellation confirmation email."`.
- **Remediation:** Handle the cancellation webhook and send a confirmation immediately:

  ```ts
  // app/api/webhooks/stripe/route.ts (add to existing handler)
  if (event.type === 'customer.subscription.updated') {
    const subscription = event.data.object
    if (subscription.cancel_at_period_end) {
      const customer = await stripe.customers.retrieve(subscription.customer as string)
      const email = 'email' in customer ? customer.email : null

      if (email) {
        await sendEmail({
          to: email,
          subject: 'Your subscription has been cancelled',
          template: 'cancellation-confirmation',
          data: {
            accessUntil: new Date(subscription.current_period_end * 1000)
              .toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }),
            supportEmail: 'support@example.com',
          },
        })
      }
    }
  }

  if (event.type === 'customer.subscription.deleted') {
    // Subscription fully ended (no grace period remaining)
    // Downgrade user to free tier in your database
    await db.user.update({
      where: { stripeCustomerId: event.data.object.customer as string },
      data: { plan: 'free' },
    })
  }
  ```

  The email content: "Your subscription has been cancelled. You will retain access to [Plan Name] features until [date]. No further charges will be made."

---

## External references

- external ftc-click-to-cancel-rule-2024 — https://www.ftc.gov/news-events/news/press-releases/2024/10/ftc-announces-final-click-cancel-rule-making-it-easier-consumers-end-unwanted-subscriptions
- external california-arl — https://leginfo.legislature.ca.gov/faces/codes_displaySection.xhtml?sectionNum=17602.&lawCode=BPC
- external eu-consumer-rights-directive-2011-83-eu — https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX%3A32011L0083

Taxons: regulatory-conformance, content-integrity

HTML version: https://auditbuffet.com/patterns/ab-002555
