GUIDES

Cancel modal

Open the NoChurn cancel flow from your cancel button, with a customer token signed by your server, your own UI or NoChurn's, and click to cancel at every step.

The cancel flow asks why the customer is leaving, shows the offer you attached to that reason (discount, pause, downgrade or talk to us), and cancels in one click if they still want to. Cancel now is visible at every step, and a requested cancellation is always carried out.

Configure the flow

In the dashboard, Cancel flow edits the reasons, offers, texts per language, feedback question and design, as a draft. Publish it to test mode first. The same is available through the API: GET /v1/flows/cancel returns the draft, PATCH /v1/flows/cancel edits it and POST /v1/flows/cancel/publish with { "to": "test" } publishes it. Offers live in a library (/v1/offers) and are attached to reasons.

The customer token

The browser never names a customer itself: your server signs a short token for the logged-in user, and the browser sends it with the publishable key.

  • Format: JWT HS256, signed with the widget signing secret (ncws_…, created in Developers or with POST /v1/account/widget_secret, shown once; rotating it invalidates the tokens signed with the old one).
  • Claims: kind: "external", value (your user id), exp (24 hours at most), and subscription: the subscription the cancel button is about (sub_…, nsub_… or its id in your billing). Always send subscription when a customer can have several.
  • Where: an authenticated endpoint of your backend that your front end calls before opening the flow. Never ship the secret to the browser or the app.
// Node, server side. Env: NOCHURN_WIDGET_SECRET (ncws_…).
import { createHmac } from "node:crypto";

const base64url = (input) => Buffer.from(input).toString("base64url");

export function signCustomerToken(userId, secret = process.env.NOCHURN_WIDGET_SECRET, ttlSeconds = 3600, subscription = undefined) {
  const claims = { kind: "external", value: String(userId), exp: Math.floor(Date.now() / 1000) + ttlSeconds };
  if (subscription) claims.subscription = String(subscription);
  const header = base64url(JSON.stringify({ alg: "HS256", typ: "JWT" }));
  const payload = base64url(JSON.stringify(claims));
  const signature = createHmac("sha256", secret).update(`${header}.${payload}`).digest("base64url");
  return `${header}.${payload}.${signature}`;
}

The publishable key (pk_test_…) only works from the origins you allow on it (Developers, or Cancel flow › Install).

Headless: your UI, NoChurn’s logic

Build the screens yourself (web, desktop or a native app) and let NoChurn decide what to show and what to do.

  1. Open a session: POST /v1/cancel_sessions { locale } with Authorization: Bearer pk_test_… and X-NoChurn-Customer-Token: <token>. The session carries its step, the texts, the reasons and the offer.
  2. Render the step and send each answer to POST /v1/cancel_sessions/{id}/answer (table below).
  3. Click to cancel. Show a visible Cancel now button at every step, wired to POST /v1/cancel_sessions/{id}/cancel. At choose_subscription, ask which one first and send { subscription } (one “Cancel this one” button per subscription): NoChurn never guesses and never cancels them all, and answers 422 subscription_required without it.
Step Answer
choose_subscription { subscription }: only when the customer has several live subscriptions and the token names none; list subscriptions and let them pick
reason { reason, comment? }
offer { accept_offer: true } or { accept_offer: false }
feedback { feedback }
confirm POST /v1/cancel_sessions/{id}/cancel (step 3)
// Browser. publishableKey: pk_test_… (your origin allowed on it); customerToken: signed by YOUR server.
const NOCHURN_API_URL = "https://api.nochurn.sh";

function nochurnHeaders(publishableKey, customerToken) {
  return { Authorization: `Bearer ${publishableKey}`, "X-NoChurn-Customer-Token": customerToken, "Content-Type": "application/json" };
}

async function call(path, publishableKey, customerToken, body) {
  const response = await fetch(`${NOCHURN_API_URL}${path}`, {
    method: "POST", headers: nochurnHeaders(publishableKey, customerToken), body: JSON.stringify(body ?? {}),
  });
  const data = await response.json();
  if (!response.ok) throw new Error(`NoChurn: ${data.error?.message} ${data.error?.fix ?? ""}`);
  return data;
}

// Opens a session: its step (choose_subscription, reason, offer, feedback, confirm), subscriptions, reasons and offer.
export function startCancelSession(publishableKey, customerToken, locale = "en") {
  return call("/v1/cancel_sessions", publishableKey, customerToken, { locale });
}

// answer: { subscription } | { reason, comment? } | { accept_offer: true | false } | { feedback }
export function answerCancelSession(sessionId, answer, publishableKey, customerToken) {
  return call(`/v1/cancel_sessions/${sessionId}/answer`, publishableKey, customerToken, answer);
}

// subscription: required at choose_subscription only (the one the customer pressed "Cancel this one" on).
export function cancelNow(sessionId, publishableKey, customerToken, subscription) {
  return call(`/v1/cancel_sessions/${sessionId}/cancel`, publishableKey, customerToken, subscription ? { subscription } : {});
}

From a server, the same session opens with the secret key and { customer, subscription }. GET /v1/cancel_sessions/{id} reads a session again after an error.

What happens on a cancellation: with Stripe or a billing tool that can cancel through its API, NoChurn cancels the subscription; with billing by API, your backend receives billing.cancel_requested and applies it. Either way the cancel_session.completed webhook tells you the outcome.

The NoChurn modal

NoChurn’s own modal is served by the browser script js.nochurn.sh/v1, with the publishable key and the same customer token. Its design comes from the flow (GET /v1/cancel_flows/{id}/theme: a preset, tokens such as colours, font, radius and spacing, the logo, a format modal, panel or page, and optional custom CSS). Custom CSS can never hide the cancel button: the script checks it at render and puts it back. The script’s reference (install tag, functions, options) will be on its own page; until then, use the headless flow above.

If the modal cannot open, the cancellation still goes through: a stable URL per publishable key, https://cancel.nochurn.sh/k/<pk_…>, receives the signed token by POST (an HTML form with a token field, locale optional), opens a hosted session and redirects to a page with one button that cancels. The token never appears in a URL. The URL is returned as cancel_fallback_url by GET /v1/cancel_flows/{id} with a publishable key.

Native apps

GET /v1/cancel_flows/default/theme gives native apps the same design tokens. Subscriptions sold through the App Store or Google Play are managed by Apple and Google: send those customers to the store’s subscription settings.