# How to build an online cancellation flow that satisfies California and New York

> A cancellation flow that meets California's click-to-cancel rule (Bus. & Prof. Code § 17602(c)–(f)), New York's "as easy as sign-up" rule (GBL § 527-a) and New York City's 2026 rule — the screens, the save-offer rules, the confirmation, and server code for Stripe Billing.

Cancellation friction is the failure behind the largest subscription enforcement outcomes on record, and the area where
the law is moving fastest. California, New York and New York City now say, in different words, the same thing: **a
customer who signed up online must be able to cancel online, immediately, at least as easily as they signed up**. This
guide turns those rules into screens and code.

## What the laws require {#requirements}

| Rule | California | New York | New York City |
|---|---|---|---|
| Online sign-ups can cancel online, at will | § 17602(d) "click to cancel" — a prominent link or button, or a pre-formatted termination email | § 527-a(1)(d): as easy to use as the sign-up, in the same medium | § 5-110.1(c), same |
| Every sign-up channel | § 17602(f): the medium used to sign up | § 527-a(1)(d-1): every medium in which consent can be given | § 5-110.1(d): in-person sign-ups also get an online option |
| No obstruction | § 17602(c)(1): cost-effective, timely, easy to use | § 527-a(1)(e): do not obstruct, condition, ignore or unreasonably delay | § 5-110.1(e), save offers included |
| Save offers | § 17602(e): only with a continuously displayed "click to cancel" option | Allowed only if they do not obstruct or delay | Same as New York |
| Phone | § 17602(c)(2): answer promptly; voicemail cancellations handled within one business day | — | — |
| After a failed attempt | — | — | § 5-110.2: refund everything charged after the first attempt to cancel |

Full text and citations: [California](https://checkbox.site/laws/california#cancellation), [New York](https://checkbox.site/laws/new-york#cancellation),
[New York City](https://checkbox.site/laws/new-york-city#cancellation). Federal law adds a "simple mechanism" to stop recurring charges
([15 U.S.C. § 8403(3)](https://checkbox.site/laws/federal#cancellation)), and other states add their own twists: Minnesota allows retention
offers only if the customer opts in, and Virginia and Maryland forbid requiring a live agent.

## The flow {#flow}

Three screens are enough, and fewer is better:

1. **Account page → "Cancel subscription"**, one click from where customers manage their account. Do not hide it
   behind "Contact us" or a help-center article. Link the same URL from your disclosure, acknowledgment email and
   reminders.
2. **Confirm.** One screen with what happens (service ends on a date, no further charges) and a button labelled
   **Cancel subscription**. If you show a save offer, it sits **beside** the cancel button, which stays visible and
   works in one click; never put the offer in the way. In Minnesota, ask first ("Would you like to hear about an offer
   before you cancel?").
3. **Done.** An on-screen confirmation with the end date and a reference, and the same in an email.

Avoid: forcing a phone call, chat or email; multiple "Are you sure?" loops; requiring a reason; a survey before the
button works; logging the customer out mid-flow; cancelling "at the end of the next billing cycle" when your
disclosure said "cancel anytime".

## Server code with Stripe Billing {#stripe}

Cancel at the end of the paid period (the common, customer-friendly choice) and record the request first, so a later
dispute can be answered with the exact time the customer asked:

```tsx title="app/api/subscription/cancel/route.ts"
import Stripe from 'stripe';
import { getSession } from '@/lib/auth';
import { db } from '@/lib/db';
import { sendCancellationEmail } from '@/lib/email';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function POST() {
  const user = await getSession();
  if (!user?.stripeSubscriptionId) return Response.json({ error: 'No active subscription' }, { status: 404 });

  // 1. Record the request before anything else (NYC refunds every charge after the first attempt).
  const request = await db.cancellationRequests.create({ userId: user.id, requestedAt: new Date() });

  // 2. Stop renewal. No retention step stands between the request and this call.
  const sub = await stripe.subscriptions.update(user.stripeSubscriptionId, { cancel_at_period_end: true });
  const endsAt = new Date(sub.items.data[0]!.current_period_end * 1000);

  // 3. Confirm on screen and by email, with the end date.
  await db.cancellationRequests.update(request.id, { completedAt: new Date(), endsAt });
  await sendCancellationEmail(user.email, { endsAt, reference: request.id });
  return Response.json({ cancelled: true, ends_at: endsAt.toISOString(), reference: request.id });
}
```

If you use the Stripe customer portal, enable **Cancel subscriptions** in its settings and link the portal from the
account page; see [Stripe customer portal](https://checkbox.site/docs/guides/stripe-customer-portal). Checkbox can also host the whole flow
for you: see [hosted cancellation](https://checkbox.site/docs/guides/cancellation#hosted).

## The confirmation email {#confirmation-email}

```bash
checkbox check email --kind cancellation_confirmation --subject "Your Acme Pro subscription is cancelled" \
  --text "We've cancelled your Acme Pro subscription. You won't be charged again. You can keep using Acme Pro until December 15, 2026. Reference: CR-1042." \
  --merchant-name Acme --price 19.99 --interval month --offline
```

## Make the Terms match {#terms}

Terms of Service that say "no cancellations", "cancel by phone only" or "cancellation requires 30 days' written notice"
contradict the online-cancellation rules and are quoted back in complaints. Checkbox reads your Terms and flags those
clauses (`terms.cancellation_policy_consistent`).

## Test it like a customer would {#test}

Checkbox's synthetic shopper signs in with a test account, finds the cancel option from the account page and cancels,
recording each step with screenshots (`cancel.online_available`, `cancel.mechanism_findable`, `cancel.flow_friction`,
`cancel.confirmation`). A [test subscription](https://checkbox.site/docs/guides/test-runs) runs the full lifecycle in Stripe test mode.
Start free: [quickstart](https://checkbox.site/docs/quickstart), or ask your coding assistant to connect the Checkbox MCP server at
`https://app.checkbox.site/mcp`.

> **Not legal advice**
>
> A practical guide, not legal advice. Requirements are drafted from the cited sources and are pending attorney review.
