The FTC Click-to-Cancel Rule (effective 2025) mandates that cancellation be available in the same number of steps — or fewer — as enrollment. Requiring users to email support to cancel while allowing online self-service enrollment is an automatic regulatory violation regardless of how simple the enrollment was. California ARL imposes the same requirement. Cancellation friction is also the primary driver of 'friendly fraud' chargebacks: users who cannot cancel quickly dispute the charge with their bank instead, and Stripe's 0.75% dispute threshold has real consequences.
Critical because requiring more steps to cancel than to enroll, or routing cancellation through support contact, is a per-subscriber regulatory violation under the FTC Click-to-Cancel Rule with civil penalties.
Implement self-service cancellation reachable in two clicks from the authenticated dashboard. The Stripe Billing Portal handles step-count compliance automatically. In app/api/billing-portal/route.ts:
export async function POST(req: Request) {
const session = await getServerSession()
if (!session?.user?.id) return new Response('Unauthorized', { status: 401 })
const portalSession = await stripe.billingPortal.sessions.create({
customer: await getStripeCustomerId(session.user.id),
return_url: `${process.env.NEXT_PUBLIC_APP_URL}/settings/billing`,
})
return Response.redirect(portalSession.url)
}
Wire a 'Cancel subscription' button in app/settings/billing/page.tsx — not buried in an 'Advanced' section. Verify in Stripe Dashboard → Billing → Customer portal that 'Allow customers to cancel subscriptions' is enabled. Document the step count: enrollment steps vs. cancellation steps.
ID: subscription-compliance.cancellation.click-to-cancel
Severity: critical
What to look for: This is the FTC's "click-to-cancel" rule, effective 2025. Count the distinct user steps required to enroll in a subscription (from clicking the first "Subscribe" button to receiving confirmation). Then count the distinct steps required to cancel. The cancellation path must not exceed the enrollment path in step count. Navigate the cancellation flow yourself: Settings → Billing (or equivalent) → Cancel. Count each distinct page load, modal, or confirmation dialog as one step. A cancellation confirmation dialog ("Are you sure?") counts as one additional step but is acceptable. An "exit survey" before cancellation is accepted practice if it is clearly skippable. Requiring the user to contact customer support to cancel is an automatic failure of this check regardless of enrollment step count. Also check: is there a "Save offer" retention interstitial? It is acceptable if it has a clear "No thanks, continue cancelling" path that does not require additional effort. Before evaluating, extract and quote the navigation labels on the path from the user dashboard to the cancellation mechanism.
Pass criteria: The number of steps to cancel is equal to or fewer than the number of steps to enroll. Cancellation is fully self-service (no email, chat, or phone required). Any retention offer has a clear, easy "continue cancelling" path. The cancellation path is discoverable from within the product's authenticated UI. Report even on pass: "Cancellation reachable in X clicks from dashboard. Self-service mechanism confirmed." At least 1 implementation must be confirmed.
Fail criteria: Cancellation requires more steps than enrollment. Cancellation requires contacting support (email, chat, phone) — this is a regulatory violation under FTC rules. The cancellation path is only available via a hidden link, not from main account settings. Exit surveys or retention flows that require a specific action to bypass (rather than a skippable step) add non-cancelable friction. Do NOT pass if cancellation requires contacting support (email, phone, chat) rather than being available as a self-service mechanism.
Skip (N/A) when: The application has no subscription or recurring billing.
Cross-reference: The separate-subscription-consent check in Enrollment verifies the consent mechanism that starts the subscription this cancellation terminates.
Detail on fail: Specify step counts. Example: "Enrollment: 3 steps (Pricing → Checkout → Confirm). Cancellation: 5 steps (Settings → Billing → Manage → Survey → Cancel). Cancellation requires 2 more steps than enrollment." or "Cancellation link found in Settings but it opens a mailto: link directing users to email support. Self-service cancellation not available.".
Remediation: Implement self-service cancellation with a step count at or below enrollment. Stripe Billing Portal handles this automatically:
// Option A — Use Stripe Billing Portal (handles cancellation in 2 steps)
// app/api/billing-portal/route.ts
export async function POST(req: Request) {
const session = await getServerSession()
if (!session?.user?.id) return new Response('Unauthorized', { status: 401 })
const customer = await getStripeCustomerId(session.user.id)
const portalSession = await stripe.billingPortal.sessions.create({
customer,
return_url: `${process.env.NEXT_PUBLIC_APP_URL}/settings/billing`,
// Configure in Stripe Dashboard: Portal → Settings → Allow cancellation
})
return Response.redirect(portalSession.url)
}
// Option B — Custom cancellation endpoint (if not using Portal)
// app/api/subscription/cancel/route.ts
export async function POST(req: Request) {
const session = await getServerSession()
if (!session?.user?.id) return new Response('Unauthorized', { status: 401 })
const { subscriptionId } = await req.json()
// Cancel at period end (user retains access until paid-through date)
await stripe.subscriptions.update(subscriptionId, {
cancel_at_period_end: true,
})
// Send cancellation confirmation email (see cancellation-confirmation check)
await sendCancellationConfirmation(session.user.email, subscriptionId)
return Response.json({ cancelled: true })
}
Wire this to a prominent "Cancel subscription" button in /settings/billing — not buried under an "Advanced" or "Danger" section.