> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nozle.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# CancellationControl

> Cancel a subscription at the end of its period, or keep it before the cancellation takes effect.

`CancellationControl` renders "Cancel subscription" for an active subscription, or "Keep my subscription" for one that is ending, and confirms the date from your backend's preview in a dialog.

**When to use**

* The subscription section of your billing settings page.
* Cancellation at the end of the period, with the exact end date from your backend.
* Keeping (uncancelling) a subscription before its end date, with the next renewal date.

[`BillingPortal`](/sdks/react/components/billing-portal) renders this control for you when you pass `cancellationActions`.

**Light**

![The Cancel subscription button, light theme](https://mintcdn.com/nozle-d78f01d8/Ae2cAgwEfN_SPqzG/images/react/components/cancellation-control/light.png?fit=max\&auto=format\&n=Ae2cAgwEfN_SPqzG\&q=85\&s=548544f6d76799a95b843bfd639ff266)

**Dark**

![The Cancel subscription button, dark theme](https://mintcdn.com/nozle-d78f01d8/Ae2cAgwEfN_SPqzG/images/react/components/cancellation-control/dark.png?fit=max\&auto=format\&n=Ae2cAgwEfN_SPqzG\&q=85\&s=92d91cd454c3e1cdf81e2efd5c5e27cb)

**Minimal**

![The Cancel subscription button, minimal theme](https://mintcdn.com/nozle-d78f01d8/Ae2cAgwEfN_SPqzG/images/react/components/cancellation-control/minimal.png?fit=max\&auto=format\&n=Ae2cAgwEfN_SPqzG\&q=85\&s=decac5ef18034d345e95032db440d235)

**Neobrutal**

![The Cancel subscription button, neobrutal theme](https://mintcdn.com/nozle-d78f01d8/Ae2cAgwEfN_SPqzG/images/react/components/cancellation-control/neobrutal.png?fit=max\&auto=format\&n=Ae2cAgwEfN_SPqzG\&q=85\&s=6f1ed091b0a6b0007581dca6543a9a9c)

**Nozle**

![The Cancel subscription button, nozle theme](https://mintcdn.com/nozle-d78f01d8/Ae2cAgwEfN_SPqzG/images/react/components/cancellation-control/nozle.png?fit=max\&auto=format\&n=Ae2cAgwEfN_SPqzG\&q=85\&s=864e26a7837db5bfb27eb5d47dc4c960)

**Nozle Dark**

![The Cancel subscription button, nozleDark theme](https://mintcdn.com/nozle-d78f01d8/Ae2cAgwEfN_SPqzG/images/react/components/cancellation-control/nozle-dark.png?fit=max\&auto=format\&n=Ae2cAgwEfN_SPqzG\&q=85\&s=0c07b8845f45cb90002ee737d8da8bce)

## Importing CancellationControl

```tsx theme={null}
import {
  BillingPortalError,
  CancellationControl,
  type CancellationActions,
  type CancellationSubscription,
} from '@nozle-js/react'
```

## Rendering CancellationControl

Pass the subscription, `actions` and `refresh`, which call your authenticated backend for the signed-in customer (see [Live data](#live-data)):

```tsx components/cancel-plan.tsx theme={null}
'use client'

export function CancelPlan(props: {
  subscription: CancellationSubscription
  actions: CancellationActions
  refresh: (signal: AbortSignal) => Promise<CancellationSubscription | null>
  onChanged: () => void
}) {
  return <CancellationControl {...props} locale="en-US" timezone="UTC" />
}
```

After every attempt, including one whose response was lost, the control re-reads the subscription with `refresh`. "Check status / retry" retries with the same idempotency key.

## Data

`subscription` and `refresh` use the `CancellationSubscription` shape:

```tsx theme={null}
export const subscription: CancellationSubscription = {
  id: '6f1c2a4e-0000-4000-8000-000000000001',
  externalId: 'sub_123',
  name: 'Pro subscription',
  status: 'active',
  endingAt: null,
  pendingPlanName: null,
}
```

### Live data

Create `actions` and `refresh` once, outside the component:

```tsx lib/cancellation-actions.ts theme={null}
async function post<T>(path: string, body: object, signal?: AbortSignal): Promise<T> {
  const response = await fetch(path, {
    method: 'POST',
    credentials: 'same-origin',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
    signal,
  })
  if (!response.ok) {
    throw new BillingPortalError(response.status === 401 || response.status === 403 ? 'unauthorized' : 'request')
  }
  return response.json()
}

export const actions: CancellationActions = {
  preview: ({ signal, ...body }) => post('/api/billing/cancellation/preview', body, signal),
  apply: ({ signal, ...body }) => post('/api/billing/cancellation', body, signal),
}

export const refresh = (signal: AbortSignal) =>
  post<CancellationSubscription | null>('/api/billing/subscription', {}, signal)
```

Your backend answers the two actions and `refresh` for the signed-in customer. Reject a changed end date on the server with HTTP 409. Use the subscription's external ID for these calls and keep its Core `id` for portal reads. See [Cancel and Keep](/sdks/react/billing-portal#cancel-and-keep) for the server side.

### Data from the API

| Callback | Receives | Returns |
| - | - | - |
| `preview` | `subscriptionId`, `operation` (`cancel` or `uncancel`), `signal` | `{ operation, effectiveAt, renewalAt }`, ISO dates; `renewalAt` may be null |
| `apply` | the same, plus `idempotencyKey` and, for `cancel`, `expectedEffectiveAt` (the preview's date) | Anything |
| `refresh` | `signal` | The subscription as `CancellationSubscription`, or `null` |

## Examples

### Cancel dialog

"Cancel subscription" opens a dialog with the end date from `preview`, a note that invoices and usage charges still apply, and "Confirm cancellation". When `pendingPlanName` is set, the dialog also says that the scheduled plan change will be removed and that keeping the subscription later will not restore it.

![The Cancel subscription dialog with the end date, a note about outstanding charges, and Close and Confirm cancellation buttons, light theme](https://mintcdn.com/nozle-d78f01d8/Ae2cAgwEfN_SPqzG/images/react/components/cancellation-control-dialog/light.png?fit=max\&auto=format\&n=Ae2cAgwEfN_SPqzG\&q=85\&s=f15299cafe0bcc15cc9aa436a69819fa)

```tsx theme={null}
const subscription: CancellationSubscription = {
  id: '6f1c2a4e-0000-4000-8000-000000000001',
  externalId: 'sub_123',
  name: 'Pro subscription',
  status: 'active',
  endingAt: null,
  pendingPlanName: 'Free',
}

export function CancelPlan({ actions, refresh }: {
  actions: CancellationActions
  refresh: (signal: AbortSignal) => Promise<CancellationSubscription | null>
}) {
  return <CancellationControl subscription={subscription} actions={actions} refresh={refresh} />
}
```

### Keep an ending subscription

When `endingAt` is set and has not passed, the button reads "Keep my subscription". Its dialog says "Your current plan will continue to renew on \<date>." and offers "Confirm keep subscription".

```tsx theme={null}
const subscription: CancellationSubscription = {
  id: '6f1c2a4e-0000-4000-8000-000000000001',
  externalId: 'sub_123',
  name: 'Pro subscription',
  status: 'active',
  endingAt: '2026-11-01T00:00:00Z',
}

export function KeepPlan({ actions, refresh }: {
  actions: CancellationActions
  refresh: (signal: AbortSignal) => Promise<CancellationSubscription | null>
}) {
  return <CancellationControl subscription={subscription} actions={actions} refresh={refresh} />
}
```

## API reference

**subscription** `CancellationSubscription` *required*

The subscription. The button shows only when `status` is `active` and `endingAt` has not passed.

**properties**

**id** `string` *required*

Core UUID, distinct from the external subscription ID.

**externalId** `string` *required*

External subscription ID, sent to `preview` and `apply` as `subscriptionId`.

**name** `string` *required*

Shown under the dialog title.

**status** `string` *required*

Subscription status, for example `active`.

**endingAt** `string | null` *required*

End date of a cancelled subscription. When set, the control offers "Keep my subscription".

**pendingPlanName** `string | null`

Name of a scheduled plan change, which a cancellation removes.

**actions** `CancellationActions` *required*

Your backend callbacks.

**properties**

**preview** `(input: CancellationInput) =\> Promise\<CancellationPreview\>` *required*

Fires when the dialog opens and receives `subscriptionId`, `operation` (`cancel` or `uncancel`) and `signal`. Returns `{ operation, effectiveAt, renewalAt }`.

**apply** `(input: CancellationApplyInput) =\> Promise\<unknown\>` *required*

Fires on confirm and receives the same, plus `idempotencyKey` and, for `cancel`, `expectedEffectiveAt`. May return anything.

**refresh** `(signal: AbortSignal) =\> Promise\<CancellationSubscription | null\>` *required*

Reads the saved subscription before each preview and after every attempt; the control checks it to decide whether the change took effect.

**onChanged** `() =\> void`

Fires after a cancellation or keep is confirmed by `refresh`.

**onError** `(error: BillingPortalError) =\> void`

Fires when a request fails and receives the `BillingPortalError`.

**locale** `string` (default: en-US)

Formats the dates (long style, for example "November 1, 2026"). The text is English.

**timezone** `string` (default: UTC)

Time zone of the dates: an IANA name, or a Nozle `TZ_` name.

**nonce** `string`

CSP nonce for the control's `<style>` element.

**appearance** `Appearance`

Theme, variables and element overrides on top of the provider's. See [Appearance](/sdks/react/styling).

### States

| State | What shows |
| - | - |
| Active subscription | "Cancel subscription". |
| Ending subscription | "Keep my subscription". |
| Dialog, loading | "Loading subscription details…". A failed preview shows "Could not load this change. Please try again." and "Try again". |
| Subscription changed since render | "Your subscription changed. Close this dialog and refresh your billing details." |
| Cancel preview | "If you confirm, your subscription will end on \<date>." and that invoices and usage charges still apply; "Confirm cancellation". |
| Keep preview | "Your current plan will continue to renew on \<date>."; "Confirm keep subscription". |
| Confirming | "Confirming subscription status…". |
| Done | "Cancellation scheduled." or "Your subscription will continue to renew." |
| Request over 30 seconds | "This request timed out. Check its status before trying again." |

The dialog closes with Escape or Close, not while busy.

### Appearance

> **Element keys**
>
> `cancellationControl` (root, also class `nozle-cancellation`).
>
> Style them with `appearance.elements`. See [Elements](/sdks/react/styling#elements).

## Related

> **[PlanChangeControl](/sdks/react/components/plan-change-control)**
>
> Upgrades, downgrades and scheduled changes.

> **[BillingPortal](/sdks/react/components/billing-portal)**
>
> The whole customer portal, with this control inside.

> **[Billing portal and self-service](/sdks/react/billing-portal)**
>
> Backend adapters and runnable examples.

> **[CurrentPlan](/sdks/react/components/current-plan)**
>
> Shows "Ends \<date>" for a cancelled subscription.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.