BROWSER · PUBLISHABLE KEY
Cancel flow (browser)
Called from your customer's browser, by the script (js.nochurn.sh) or your own code, with a publishable key (pk_test_…)
and a customer token signed by your server. See Authentication.
The Cancel session object
The cancel flow: its configuration, design, sessions and the customer's answers.
ATTRIBUTES
- object enum
- cancel_session
- id string
- livemode boolean
- step enum
- choose_subscription
- reason
- offer
- feedback
- confirm
- done
- outcome enum | null
- saved
- cancelled
- abandoned
- locale string
- holdout boolean
- published boolean
Show 19 more attributesHide 19 attributes
- customer object | null
- subscription object | null
The subscription this session targets (offers, saved MRR, cancellation). Null at choose_subscription, for an unknown customer, or without a live subscription.
Show child attributesHide child attributes
- id string
- plan string | null
- status enum
- incomplete
- incomplete_expired
- trialing
- active
- past_due
- canceled
- unpaid
- paused
- amount_cents integer
Price per interval (quantity included), minor unit.
- currency string | null
- interval string | null
- interval_count integer
-
Next renewal.
- cancel_at_period_end boolean
-
- subscriptions array of objects
At choose_subscription only (several live subscriptions, none named by the token's
subscriptionclaim): the customer picks one (answer { subscription }) or cancels one (cancel { subscription }). Empty otherwise.Show child attributesHide child attributes
- id string
- plan string | null
- status enum
- incomplete
- incomplete_expired
- trialing
- active
- past_due
- canceled
- unpaid
- paused
- amount_cents integer
Price per interval (quantity included), minor unit.
- currency string | null
- interval string | null
- interval_count integer
-
Next renewal.
- cancel_at_period_end boolean
-
- reasons array of objects
- offer object | null
Show child attributesHide child attributes
- id string
- kind enum
- discount
- pause
- downgrade
- talk
- title string
In the session's language, with the account's text for that offer kind (texts.<lang>.offer_<kind>) when set.
- url string | null
talk: the booking link.
- percent integer | null
discount: percent off.
- months integer | null
discount: how many months; pause: how long.
- plan_name string | null
downgrade: the target plan's name, when the offer has one.
-
- talk_url string | null
- texts object
- feedback_enabled boolean
Same as feedback.enabled (kept for older widgets).
- feedback object
- theme object
This session's design: the flow's, or its segment's / A/B variant's.
Show child attributesHide child attributes
- object enum
- cancel_flow_theme
- preset enum
- light
- dark
- playful
- custom_css
- headless
- format enum
modal (centred), panel (side panel) or page (full page).
- modal
- panel
- page
- tokens object
The --nc-* custom properties (--nc-accent-text for accent_text). Enumerated values are mapped to CSS by the widget.
Show child attributesHide child attributes
- bg string
- surface string
- text string
- muted string
- accent string
- accent_text string
- border string
- danger string
-
A font stack. Its first family loads from font_url when it is an allowed Google Font.
-
Base size, px.
-
Modal corners, px.
-
Buttons and fields, px.
-
Backdrop opacity.
-
Modal width, px.
- shadow enum
- none
- sm
- md
- lg
- spacing enum
Density.
- compact
- regular
- comfortable
-
px.
- border_style enum
- solid
- dashed
- none
-
Title size = font_size × font_scale.
- font_weight_title enum
- 400
- 500
- 600
- 700
-
- font_url string | null
Google Fonts stylesheet of the font token's first family (allowed fonts only), else null.
- brand enum
What the header shows: logo (logo_url, else app_name), name (app_name) or none (nothing). logo_url and app_name already follow it: null when the header must not show them.
- logo
- name
- none
- logo_url string | null
https URL of the logo (uploaded, or set in the editor) when brand is logo, else null (show app_name).
- app_name string | null
The account's name: the text shown in place of a logo; null when brand is none.
- end_screen object
The modal's last step (draft.design.end_screen). A cancelled customer's end screen is always calm, whatever the style.
Show child attributesHide child attributes
- style enum
Saved customer: celebrate (default), glow or quiet.
- celebrate
- glow
- quiet
-
Saved screen only: closes by itself after this (default 6); null = never.
-
- custom_css string
Cleaned on the server (no @import, url() only to Google Fonts).
- cancel_button_check enum
- ok
- fallback_applied
- classes array of enums
- nc-modal
- nc-title
- nc-reason
- nc-offer
- nc-btn-primary
- nc-btn-cancel
- nc-footer
-
- cancel_url_now string | null
Shown once, when the session is created: cancels at any step.
- cancel_status enum | null
- pending
- done
- requested
- failed
- result object | null
Once an offer is accepted: outcome saved (discount, pause, downgrade), or a talk offer accepted (the session goes on). Null otherwise.
Show child attributesHide child attributes
- kind enum
- discount
- pause
- downgrade
- talk
- currency string | null
- regular_amount_cents integer | null
The price per interval before the offer (quantity included). Null for talk.
-
discount, downgrade: the next invoice's date (the provider's, else the renewal date).
- next_invoice_amount_cents integer | null
discount, downgrade: the next invoice's amount from the provider (Stripe: upcoming invoice preview, read once when the offer is applied). Null when the provider cannot tell.
-
discount: when the usual price comes back (acceptance + the offer's months).
-
discount: the invoices due before discounted_until (12 at most), at next_invoice_amount_cents. [] otherwise.
Show child attributesHide child attributes
- amount_cents integer | null
-
pause: the resume date (null when your billing pauses it, billing by API).
- plan_name string | null
downgrade: the new plan, when the offer names it.
-
-
Cancelled: access lasts until then (cancel_effective_at: the period end, or now when immediate). Null otherwise, and while the cancellation is pending.
- confirmation_email_sent boolean
A cancellation confirmation email was actually queued. NoChurn sends none today: always false (the widget hides that fact).
- review object | null
Asked on the last step to happy leavers only, once per customer, never tied to an offer.
Schema: CancelSession
{
"object": "cancel_session",
"id": "ses_gXwed1ERtHMt3LtB",
"livemode": false,
"step": "offer",
"outcome": null,
"locale": "fr",
"holdout": false,
"published": true,
"customer": {
"id": "cst_SS392NRTjKfXKLRS",
"stripe_customer_id": "cus_1",
"external_id": "user_42"
},
"subscription": {
"id": "nsub_UXXWSnvbbahnokAE",
"plan": null,
"status": "active",
"amount_cents": 4900,
"currency": "eur",
"interval": "month",
"interval_count": 1,
"current_period_end": "2026-11-24T10:00:00Z",
"cancel_at_period_end": false
},
"subscriptions": [],
"reasons": [
{
"key": "too_expensive",
"label": "C'est trop cher",
"free_text": false
},
{
"key": "missing_feature",
"label": "Il manque une fonctionnalité",
"free_text": false
},
{
"key": "not_using",
"label": "Je ne l'utilise pas assez",
"free_text": false
},
{
"key": "switching",
"label": "Je passe à un autre outil",
"free_text": false
},
{
"key": "temporary",
"label": "J'ai juste besoin d'une pause",
"free_text": false
},
{
"key": "project_finished",
"label": "Mon projet est terminé",
"free_text": false
},
{
"key": "other",
"label": "Autre chose",
"free_text": true
}
],
"offer": {
"id": "off_A8q4hT3LvQy4tvDX",
"kind": "discount",
"title": "30 % de moins, 3 mois : restez !",
"url": null,
"percent": 30,
"months": 3,
"plan_name": null
},
"talk_url": null,
"texts": {
"review_ask": "Laisseriez-vous un court avis sur {{platform}} ? Aucune récompense, juste votre avis sincère.",
"review_cta": "Laisser un avis",
"title": "Avant de partir",
"reason_prompt": "Pourquoi résiliez-vous ?",
"other_placeholder": "Dites-nous en plus (facultatif)",
"offer_title": "Une dernière chose",
"accept": "Accepter l'offre",
"decline": "Non merci, continuer",
"feedback_prompt": "Qu'aurions-nous pu faire mieux ?",
"feedback_submit": "Continuer",
"confirm_title": "Résilier votre abonnement ?",
"confirm_body": "Vous gardez l'accès jusqu'à la fin de la période payée.",
"cancel_now": "Résilier mon abonnement",
"saved_title": "Merci de rester !",
"cancelled_title": "Votre abonnement est résilié",
"cancelled_body": "Vous gardez l'accès jusqu'à la fin de la période payée.",
"offer_discount": "{{percent}} % de moins, {{months}} mois : restez !",
"offer_pause": "Mettre en pause {{months}} mois, gratuitement",
"offer_downgrade": "Passer à {{plan}}",
"offer_talk": "Parler à l'équipe",
"choose_subscription_title": "Quel abonnement ?",
"choose_subscription_body": "Vous avez plusieurs abonnements. Choisissez celui que vous voulez résilier.",
"choose_subscription_select": "Continuer avec celui-ci",
"cancel_this_one": "Résilier celui-ci",
"renews_on": "Renouvellement le {{date}}"
},
"feedback_enabled": true,
"feedback": {
"enabled": true,
"required": false
},
"theme": {
"object": "cancel_flow_theme",
"preset": "light",
"format": "modal",
"tokens": {
"bg": "#f5f6fa",
"surface": "#ffffff",
"text": "#1c1e4a",
"muted": "#686b85",
"accent": "#4637d9",
"accent_text": "#ffffff",
"border": "#e3e5ec",
"danger": "#c2410c",
"font": "system-ui, sans-serif",
"font_size": 15,
"radius": 12,
"radius_control": 8,
"overlay": 0.5,
"max_width": 480,
"shadow": "md",
"spacing": "regular",
"border_width": 1,
"border_style": "solid",
"font_scale": 1.25,
"font_weight_title": 600
},
"font_url": null,
"brand": "logo",
"logo_url": null,
"app_name": "Acme",
"end_screen": {
"style": "celebrate",
"auto_close_seconds": 6
},
"custom_css": "",
"cancel_button_check": "ok",
"classes": [
"nc-modal",
"nc-title",
"nc-reason",
"nc-offer",
"nc-btn-primary",
"nc-btn-cancel",
"nc-footer"
]
},
"cancel_url_now": null,
"cancel_status": null,
"cancel_effective_at": null,
"result": null,
"access_until": null,
"confirmation_email_sent": false,
"review": null,
"expires_at": "2026-11-05T10:00:00Z",
"created_at": "2026-11-04T10:00:00Z"
} Open the cancel flow for a customer
From the browser: publishable key + X-NoChurn-Customer-Token + allowed Origin. From a server: secret key and customer. Click to cancel: POST /cancel works at every step; cancel_url_now is shown once. The subscription it targets: the token's subscription claim (browser) or subscription (server), else the customer's only live one; several and none named: the session starts at choose_subscription (DECISIONS 72).
- Publishable key (
pk_test_…) + customer token (X-NoChurn-Customer-Token), from an allowed Origin. - From a server: a secret key (
nc_test_…) and thecustomerinstead of a token.
HEADER PARAMETERS
- Origin string
BODY PARAMETERS
- customer string
- subscription string
Secret key only (from a browser: the token's
subscriptionclaim). nsub_…, sub_… or its id in your billing; a live subscription of that customer, else 404 subscription_not_found. - locale enum
- en
- fr
- de
- it
- pt
RETURNS
-
201session at the reason step — the Cancel session object
ERRORS
-
401unauthorized a publishable key without a customer token -
403forbidden an origin that is not allowed
curl https://api.nochurn.sh/v1/cancel_sessions \
-H "Authorization: Bearer pk_test_…" \
-H "X-NoChurn-Customer-Token: eyJhbGciOiJIUzI1NiJ9…" \
-H "Content-Type: application/json" \
-d '{
"customer": "user_42"
}'const res = await fetch("https://api.nochurn.sh/v1/cancel_sessions", {
method: "POST",
headers: {
Authorization: "Bearer pk_test_…",
"X-NoChurn-Customer-Token": "eyJhbGciOiJIUzI1NiJ9…",
"Content-Type": "application/json",
},
body: JSON.stringify({
"customer": "user_42"
}),
});
const cancelSession = await res.json();import requests
res = requests.post(
"https://api.nochurn.sh/v1/cancel_sessions",
headers={
"Authorization": "Bearer pk_test_…",
"X-NoChurn-Customer-Token": "eyJhbGciOiJIUzI1NiJ9…",
},
json={
"customer": "user_42"
},
)
cancel_session = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/cancel_sessions")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer pk_test_…"
req["X-NoChurn-Customer-Token"] = "eyJhbGciOiJIUzI1NiJ9…"
req["Content-Type"] = "application/json"
req.body = {
"customer" => "user_42"
}.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
cancel_session = JSON.parse(res.body) {
"object": "cancel_session",
"id": "ses_CWSSquPBUBep38LF",
"livemode": false,
"step": "reason",
"outcome": null,
"locale": "en",
"holdout": false,
"published": true,
"customer": {
"id": "cst_8spCLa6W9qQzhEpt",
"stripe_customer_id": "cus_1",
"external_id": "user_42"
},
"subscription": {
"id": "nsub_dNjp6zubp5wmUDhA",
"plan": null,
"status": "active",
"amount_cents": 4900,
"currency": "eur",
"interval": "month",
"interval_count": 1,
"current_period_end": "2026-11-24T10:00:00Z",
"cancel_at_period_end": false
},
"subscriptions": [],
"reasons": [
{
"key": "too_expensive",
"label": "It's too expensive",
"free_text": false
},
{
"key": "missing_feature",
"label": "A feature is missing",
"free_text": false
},
{
"key": "not_using",
"label": "I don't use it enough",
"free_text": false
},
{
"key": "switching",
"label": "I'm switching to another tool",
"free_text": false
},
{
"key": "temporary",
"label": "I only need a break",
"free_text": false
},
{
"key": "project_finished",
"label": "My project is finished",
"free_text": false
},
{
"key": "other",
"label": "Something else",
"free_text": true
}
],
"offer": null,
"talk_url": null,
"texts": {
"review_ask": "Would you leave a short review on {{platform}}? No reward, just your honest opinion.",
"review_cta": "Leave a review",
"title": "Before you go",
"reason_prompt": "Why are you cancelling?",
"other_placeholder": "Tell us more (optional)",
"offer_title": "One last thing",
"accept": "Accept the offer",
"decline": "No thanks, continue",
"feedback_prompt": "Anything we could have done better?",
"feedback_submit": "Continue",
"confirm_title": "Cancel your subscription?",
"confirm_body": "You keep access until the end of the paid period.",
"cancel_now": "Cancel my subscription",
"saved_title": "Thanks for staying!",
"cancelled_title": "Your subscription is cancelled",
"cancelled_body": "You keep access until the end of the paid period.",
"offer_discount": "{{percent}}% off for {{months}} months",
"offer_pause": "Pause for {{months}} months, free",
"offer_downgrade": "Switch to {{plan}}",
"offer_talk": "Talk to the team",
"choose_subscription_title": "Which subscription?",
"choose_subscription_body": "You have several subscriptions. Choose the one you want to cancel.",
"choose_subscription_select": "Continue with this one",
"cancel_this_one": "Cancel this one",
"renews_on": "Renews on {{date}}"
},
"feedback_enabled": true,
"feedback": {
"enabled": true,
"required": false
},
"theme": {
"object": "cancel_flow_theme",
"preset": "light",
"format": "modal",
"tokens": {
"bg": "#f5f6fa",
"surface": "#ffffff",
"text": "#1c1e4a",
"muted": "#686b85",
"accent": "#4637d9",
"accent_text": "#ffffff",
"border": "#e3e5ec",
"danger": "#c2410c",
"font": "system-ui, sans-serif",
"font_size": 15,
"radius": 12,
"radius_control": 8,
"overlay": 0.5,
"max_width": 480,
"shadow": "md",
"spacing": "regular",
"border_width": 1,
"border_style": "solid",
"font_scale": 1.25,
"font_weight_title": 600
},
"font_url": null,
"brand": "logo",
"logo_url": null,
"app_name": "Acme",
"end_screen": {
"style": "celebrate",
"auto_close_seconds": 6
},
"custom_css": "",
"cancel_button_check": "ok",
"classes": [
"nc-modal",
"nc-title",
"nc-reason",
"nc-offer",
"nc-btn-primary",
"nc-btn-cancel",
"nc-footer"
]
},
"cancel_url_now": "http://localhost:3000/x/Il4clEDtUMUqj9it2-O37qsHnhoIWbq8",
"cancel_status": null,
"cancel_effective_at": null,
"result": null,
"access_until": null,
"confirmation_email_sent": false,
"review": null,
"expires_at": "2026-11-05T10:00:00Z",
"created_at": "2026-11-04T10:00:00Z"
} Read a cancel session (widget: reload after an error)
Publishable key + the customer token the session was opened with + allowed Origin, or a secret key. cancel_url_now is only given when the session is created.
- Publishable key (
pk_test_…) + customer token (X-NoChurn-Customer-Token), from an allowed Origin. - From a server: a secret key (
nc_test_…) and thecustomerinstead of a token.
PATH PARAMETERS
HEADER PARAMETERS
- X-NoChurn-Customer-Token string
- Origin string
RETURNS
-
200the session at its current step — the Cancel session object
ERRORS
-
404not_found a session of another customer
curl https://api.nochurn.sh/v1/cancel_sessions/ses_cXpnGr9TadrEogEp \
-H "Authorization: Bearer pk_test_…" \
-H "X-NoChurn-Customer-Token: eyJhbGciOiJIUzI1NiJ9…"const res = await fetch("https://api.nochurn.sh/v1/cancel_sessions/ses_cXpnGr9TadrEogEp", {
headers: {
Authorization: "Bearer pk_test_…",
"X-NoChurn-Customer-Token": "eyJhbGciOiJIUzI1NiJ9…",
},
});
const cancelSession = await res.json();import requests
res = requests.get(
"https://api.nochurn.sh/v1/cancel_sessions/ses_cXpnGr9TadrEogEp",
headers={
"Authorization": "Bearer pk_test_…",
"X-NoChurn-Customer-Token": "eyJhbGciOiJIUzI1NiJ9…",
},
)
cancel_session = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/cancel_sessions/ses_cXpnGr9TadrEogEp")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer pk_test_…"
req["X-NoChurn-Customer-Token"] = "eyJhbGciOiJIUzI1NiJ9…"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
cancel_session = JSON.parse(res.body) {
"object": "cancel_session",
"id": "ses_cXpnGr9TadrEogEp",
"livemode": false,
"step": "reason",
"outcome": null,
"locale": "en",
"holdout": false,
"published": true,
"customer": {
"id": "cst_czsHTg8pzuqqXMB7",
"stripe_customer_id": "cus_1",
"external_id": "user_42"
},
"subscription": {
"id": "nsub_FDNQxWAvM9FaEmV7",
"plan": null,
"status": "active",
"amount_cents": 4900,
"currency": "eur",
"interval": "month",
"interval_count": 1,
"current_period_end": "2026-11-24T10:00:00Z",
"cancel_at_period_end": false
},
"subscriptions": [],
"reasons": [
{
"key": "too_expensive",
"label": "It's too expensive",
"free_text": false
},
{
"key": "missing_feature",
"label": "A feature is missing",
"free_text": false
},
{
"key": "not_using",
"label": "I don't use it enough",
"free_text": false
},
{
"key": "switching",
"label": "I'm switching to another tool",
"free_text": false
},
{
"key": "temporary",
"label": "I only need a break",
"free_text": false
},
{
"key": "project_finished",
"label": "My project is finished",
"free_text": false
},
{
"key": "other",
"label": "Something else",
"free_text": true
}
],
"offer": null,
"talk_url": null,
"texts": {
"review_ask": "Would you leave a short review on {{platform}}? No reward, just your honest opinion.",
"review_cta": "Leave a review",
"title": "Before you go",
"reason_prompt": "Why are you cancelling?",
"other_placeholder": "Tell us more (optional)",
"offer_title": "One last thing",
"accept": "Accept the offer",
"decline": "No thanks, continue",
"feedback_prompt": "Anything we could have done better?",
"feedback_submit": "Continue",
"confirm_title": "Cancel your subscription?",
"confirm_body": "You keep access until the end of the paid period.",
"cancel_now": "Cancel my subscription",
"saved_title": "Thanks for staying!",
"cancelled_title": "Your subscription is cancelled",
"cancelled_body": "You keep access until the end of the paid period.",
"offer_discount": "{{percent}}% off for {{months}} months",
"offer_pause": "Pause for {{months}} months, free",
"offer_downgrade": "Switch to {{plan}}",
"offer_talk": "Talk to the team",
"choose_subscription_title": "Which subscription?",
"choose_subscription_body": "You have several subscriptions. Choose the one you want to cancel.",
"choose_subscription_select": "Continue with this one",
"cancel_this_one": "Cancel this one",
"renews_on": "Renews on {{date}}"
},
"feedback_enabled": true,
"feedback": {
"enabled": true,
"required": false
},
"theme": {
"object": "cancel_flow_theme",
"preset": "light",
"format": "modal",
"tokens": {
"bg": "#f5f6fa",
"surface": "#ffffff",
"text": "#1c1e4a",
"muted": "#686b85",
"accent": "#4637d9",
"accent_text": "#ffffff",
"border": "#e3e5ec",
"danger": "#c2410c",
"font": "system-ui, sans-serif",
"font_size": 15,
"radius": 12,
"radius_control": 8,
"overlay": 0.5,
"max_width": 480,
"shadow": "md",
"spacing": "regular",
"border_width": 1,
"border_style": "solid",
"font_scale": 1.25,
"font_weight_title": 600
},
"font_url": null,
"brand": "logo",
"logo_url": null,
"app_name": "Acme",
"end_screen": {
"style": "celebrate",
"auto_close_seconds": 6
},
"custom_css": "",
"cancel_button_check": "ok",
"classes": [
"nc-modal",
"nc-title",
"nc-reason",
"nc-offer",
"nc-btn-primary",
"nc-btn-cancel",
"nc-footer"
]
},
"cancel_url_now": null,
"cancel_status": null,
"cancel_effective_at": null,
"result": null,
"access_until": null,
"confirmation_email_sent": false,
"review": null,
"expires_at": "2026-11-05T10:00:00Z",
"created_at": "2026-11-04T10:00:00Z"
} Cancel flow config for the widget or a native app
- Publishable key (
pk_test_…). A secret key works too.
PATH PARAMETERS
-
default or flw_…
QUERY PARAMETERS
- locale string
RETURNS
-
200texts in the customer's language, reasons, theme — a CancelFlow
curl https://api.nochurn.sh/v1/cancel_flows/flw_EmdD2FFD6aiqGNqz \
-H "Authorization: Bearer pk_test_…"const res = await fetch("https://api.nochurn.sh/v1/cancel_flows/flw_EmdD2FFD6aiqGNqz", {
headers: {
Authorization: "Bearer pk_test_…",
},
});
const cancelFlow = await res.json();import requests
res = requests.get(
"https://api.nochurn.sh/v1/cancel_flows/flw_EmdD2FFD6aiqGNqz",
headers={
"Authorization": "Bearer pk_test_…",
},
)
cancel_flow = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/cancel_flows/flw_EmdD2FFD6aiqGNqz")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer pk_test_…"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
cancel_flow = JSON.parse(res.body) {
"object": "cancel_flow",
"id": "flw_EmdD2FFD6aiqGNqz",
"published": true,
"version": 1,
"locale": "de",
"texts": {
"review_ask": "Würden Sie eine kurze Bewertung auf {{platform}} hinterlassen? Keine Belohnung, nur Ihre ehrliche Meinung.",
"review_cta": "Bewertung schreiben",
"title": "Bevor Sie gehen",
"reason_prompt": "Warum kündigen Sie?",
"other_placeholder": "Erzählen Sie uns mehr (optional)",
"offer_title": "Eine letzte Sache",
"accept": "Angebot annehmen",
"decline": "Nein danke, weiter",
"feedback_prompt": "Was hätten wir besser machen können?",
"feedback_submit": "Weiter",
"confirm_title": "Abonnement kündigen?",
"confirm_body": "Sie behalten den Zugang bis zum Ende des bezahlten Zeitraums.",
"cancel_now": "Mein Abonnement kündigen",
"saved_title": "Danke, dass Sie bleiben!",
"cancelled_title": "Ihr Abonnement ist gekündigt",
"cancelled_body": "Sie behalten den Zugang bis zum Ende des bezahlten Zeitraums.",
"offer_discount": "{{percent}} % Rabatt für {{months}} Monate",
"offer_pause": "{{months}} Monate kostenlos pausieren",
"offer_downgrade": "Zu {{plan}} wechseln",
"offer_talk": "Mit dem Team sprechen",
"choose_subscription_title": "Welches Abonnement?",
"choose_subscription_body": "Sie haben mehrere Abonnements. Wählen Sie das, das Sie kündigen möchten.",
"choose_subscription_select": "Mit diesem fortfahren",
"cancel_this_one": "Dieses kündigen",
"renews_on": "Verlängert sich am {{date}}"
},
"reasons": [
{
"key": "too_expensive",
"label": "Es ist zu teuer",
"free_text": false
},
{
"key": "missing_feature",
"label": "Eine Funktion fehlt",
"free_text": false
},
{
"key": "not_using",
"label": "Ich nutze es zu wenig",
"free_text": false
},
{
"key": "switching",
"label": "Ich wechsle zu einem anderen Tool",
"free_text": false
},
{
"key": "temporary",
"label": "Ich brauche nur eine Pause",
"free_text": false
},
{
"key": "project_finished",
"label": "Mein Projekt ist abgeschlossen",
"free_text": false
},
{
"key": "other",
"label": "Etwas anderes",
"free_text": true
}
],
"feedback_enabled": true,
"feedback": {
"enabled": true,
"required": false
},
"theme": {
"object": "cancel_flow_theme",
"preset": "light",
"format": "modal",
"tokens": {
"bg": "#f5f6fa",
"surface": "#ffffff",
"text": "#1c1e4a",
"muted": "#686b85",
"accent": "#4637d9",
"accent_text": "#ffffff",
"border": "#e3e5ec",
"danger": "#c2410c",
"font": "system-ui, sans-serif",
"font_size": 15,
"radius": 12,
"radius_control": 8,
"overlay": 0.5,
"max_width": 480,
"shadow": "md",
"spacing": "regular",
"border_width": 1,
"border_style": "solid",
"font_scale": 1.25,
"font_weight_title": 600
},
"font_url": null,
"brand": "logo",
"logo_url": null,
"app_name": "Acme",
"end_screen": {
"style": "celebrate",
"auto_close_seconds": 6
},
"custom_css": "",
"cancel_button_check": "ok",
"classes": [
"nc-modal",
"nc-title",
"nc-reason",
"nc-offer",
"nc-btn-primary",
"nc-btn-cancel",
"nc-footer"
]
},
"languages": ["en", "fr", "de", "it", "pt"],
"cancel_fallback_url": null
} Answer the current step
One body per step: choose_subscription → { subscription } (one of the session's subscriptions, DECISIONS 72); reason → { reason, comment? } (comment for a free-text reason); offer → { accept_offer }; feedback → { feedback? } (required when feedback.required). Cancelling never needs an answer: POST /cancel.
- Publishable key (
pk_test_…) + customer token (X-NoChurn-Customer-Token), from an allowed Origin. - From a server: a secret key (
nc_test_…) and thecustomerinstead of a token.
PATH PARAMETERS
HEADER PARAMETERS
- X-NoChurn-Customer-Token string
- Origin string
BODY PARAMETERS
- subscription string
choose_subscription step: one of the customer's live subscriptions (nsub_…, sub_… or its id in your billing).
- reason string
Reason step: one of the session's reasons[].key.
-
Reason step: free text (reasons with free_text).
- accept_offer boolean
Offer step: true keeps the subscription with the offer (talk: talk_url, the session goes on).
-
Feedback step: optional unless feedback.required.
RETURNS
-
200the next step, with the offer in the account's words — the Cancel session object
ERRORS
-
400invalid_request a required feedback left empty -
404subscription_not_found choose_subscription: not one of the customer's live subscriptions
curl https://api.nochurn.sh/v1/cancel_sessions/ses_XsgUyosnL9TPcZiz/answer \
-H "Authorization: Bearer pk_test_…" \
-H "X-NoChurn-Customer-Token: eyJhbGciOiJIUzI1NiJ9…" \
-H "Content-Type: application/json" \
-d '{
"reason": "too_expensive"
}'const res = await fetch("https://api.nochurn.sh/v1/cancel_sessions/ses_XsgUyosnL9TPcZiz/answer", {
method: "POST",
headers: {
Authorization: "Bearer pk_test_…",
"X-NoChurn-Customer-Token": "eyJhbGciOiJIUzI1NiJ9…",
"Content-Type": "application/json",
},
body: JSON.stringify({
"reason": "too_expensive"
}),
});
const cancelSession = await res.json();import requests
res = requests.post(
"https://api.nochurn.sh/v1/cancel_sessions/ses_XsgUyosnL9TPcZiz/answer",
headers={
"Authorization": "Bearer pk_test_…",
"X-NoChurn-Customer-Token": "eyJhbGciOiJIUzI1NiJ9…",
},
json={
"reason": "too_expensive"
},
)
cancel_session = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/cancel_sessions/ses_XsgUyosnL9TPcZiz/answer")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer pk_test_…"
req["X-NoChurn-Customer-Token"] = "eyJhbGciOiJIUzI1NiJ9…"
req["Content-Type"] = "application/json"
req.body = {
"reason" => "too_expensive"
}.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
cancel_session = JSON.parse(res.body) {
"object": "cancel_session",
"id": "ses_gXwed1ERtHMt3LtB",
"livemode": false,
"step": "offer",
"outcome": null,
"locale": "fr",
"holdout": false,
"published": true,
"customer": {
"id": "cst_SS392NRTjKfXKLRS",
"stripe_customer_id": "cus_1",
"external_id": "user_42"
},
"subscription": {
"id": "nsub_UXXWSnvbbahnokAE",
"plan": null,
"status": "active",
"amount_cents": 4900,
"currency": "eur",
"interval": "month",
"interval_count": 1,
"current_period_end": "2026-11-24T10:00:00Z",
"cancel_at_period_end": false
},
"subscriptions": [],
"reasons": [
{
"key": "too_expensive",
"label": "C'est trop cher",
"free_text": false
},
{
"key": "missing_feature",
"label": "Il manque une fonctionnalité",
"free_text": false
},
{
"key": "not_using",
"label": "Je ne l'utilise pas assez",
"free_text": false
},
{
"key": "switching",
"label": "Je passe à un autre outil",
"free_text": false
},
{
"key": "temporary",
"label": "J'ai juste besoin d'une pause",
"free_text": false
},
{
"key": "project_finished",
"label": "Mon projet est terminé",
"free_text": false
},
{
"key": "other",
"label": "Autre chose",
"free_text": true
}
],
"offer": {
"id": "off_A8q4hT3LvQy4tvDX",
"kind": "discount",
"title": "30 % de moins, 3 mois : restez !",
"url": null,
"percent": 30,
"months": 3,
"plan_name": null
},
"talk_url": null,
"texts": {
"review_ask": "Laisseriez-vous un court avis sur {{platform}} ? Aucune récompense, juste votre avis sincère.",
"review_cta": "Laisser un avis",
"title": "Avant de partir",
"reason_prompt": "Pourquoi résiliez-vous ?",
"other_placeholder": "Dites-nous en plus (facultatif)",
"offer_title": "Une dernière chose",
"accept": "Accepter l'offre",
"decline": "Non merci, continuer",
"feedback_prompt": "Qu'aurions-nous pu faire mieux ?",
"feedback_submit": "Continuer",
"confirm_title": "Résilier votre abonnement ?",
"confirm_body": "Vous gardez l'accès jusqu'à la fin de la période payée.",
"cancel_now": "Résilier mon abonnement",
"saved_title": "Merci de rester !",
"cancelled_title": "Votre abonnement est résilié",
"cancelled_body": "Vous gardez l'accès jusqu'à la fin de la période payée.",
"offer_discount": "{{percent}} % de moins, {{months}} mois : restez !",
"offer_pause": "Mettre en pause {{months}} mois, gratuitement",
"offer_downgrade": "Passer à {{plan}}",
"offer_talk": "Parler à l'équipe",
"choose_subscription_title": "Quel abonnement ?",
"choose_subscription_body": "Vous avez plusieurs abonnements. Choisissez celui que vous voulez résilier.",
"choose_subscription_select": "Continuer avec celui-ci",
"cancel_this_one": "Résilier celui-ci",
"renews_on": "Renouvellement le {{date}}"
},
"feedback_enabled": true,
"feedback": {
"enabled": true,
"required": false
},
"theme": {
"object": "cancel_flow_theme",
"preset": "light",
"format": "modal",
"tokens": {
"bg": "#f5f6fa",
"surface": "#ffffff",
"text": "#1c1e4a",
"muted": "#686b85",
"accent": "#4637d9",
"accent_text": "#ffffff",
"border": "#e3e5ec",
"danger": "#c2410c",
"font": "system-ui, sans-serif",
"font_size": 15,
"radius": 12,
"radius_control": 8,
"overlay": 0.5,
"max_width": 480,
"shadow": "md",
"spacing": "regular",
"border_width": 1,
"border_style": "solid",
"font_scale": 1.25,
"font_weight_title": 600
},
"font_url": null,
"brand": "logo",
"logo_url": null,
"app_name": "Acme",
"end_screen": {
"style": "celebrate",
"auto_close_seconds": 6
},
"custom_css": "",
"cancel_button_check": "ok",
"classes": [
"nc-modal",
"nc-title",
"nc-reason",
"nc-offer",
"nc-btn-primary",
"nc-btn-cancel",
"nc-footer"
]
},
"cancel_url_now": null,
"cancel_status": null,
"cancel_effective_at": null,
"result": null,
"access_until": null,
"confirmation_email_sent": false,
"review": null,
"expires_at": "2026-11-05T10:00:00Z",
"created_at": "2026-11-04T10:00:00Z"
} Cancel now (click to cancel, at any step)
Works at every step, after expiry, for the holdout, an unpublished flow and an unknown customer. Idempotent: a retry never cancels twice. cancel_status: done (Stripe), requested (your billing, billing.cancel_requested), pending (NoChurn retries until it is done). At choose_subscription (several live subscriptions, none chosen) the body names the one to cancel; without it, 422 subscription_required: never a guess, never all of them (DECISIONS 72).
- Publishable key (
pk_test_…) + customer token (X-NoChurn-Customer-Token), from an allowed Origin. - From a server: a secret key (
nc_test_…) and thecustomerinstead of a token.
PATH PARAMETERS
HEADER PARAMETERS
- X-NoChurn-Customer-Token string
- Origin string
BODY PARAMETERS
- subscription string
choose_subscription step: the one to cancel (nsub_…, sub_… or its id in your billing).
RETURNS
-
200cancelled — the Cancel session object
ERRORS
-
422subscription_required several live subscriptions and none named
curl https://api.nochurn.sh/v1/cancel_sessions/ses_XsgUyosnL9TPcZiz/cancel \
-H "Authorization: Bearer pk_test_…" \
-H "X-NoChurn-Customer-Token: eyJhbGciOiJIUzI1NiJ9…" \
-H "Content-Type: application/json" \
-d '{}'const res = await fetch("https://api.nochurn.sh/v1/cancel_sessions/ses_XsgUyosnL9TPcZiz/cancel", {
method: "POST",
headers: {
Authorization: "Bearer pk_test_…",
"X-NoChurn-Customer-Token": "eyJhbGciOiJIUzI1NiJ9…",
"Content-Type": "application/json",
},
body: JSON.stringify({}),
});
const cancelSession = await res.json();import requests
res = requests.post(
"https://api.nochurn.sh/v1/cancel_sessions/ses_XsgUyosnL9TPcZiz/cancel",
headers={
"Authorization": "Bearer pk_test_…",
"X-NoChurn-Customer-Token": "eyJhbGciOiJIUzI1NiJ9…",
},
json={},
)
cancel_session = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/cancel_sessions/ses_XsgUyosnL9TPcZiz/cancel")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer pk_test_…"
req["X-NoChurn-Customer-Token"] = "eyJhbGciOiJIUzI1NiJ9…"
req["Content-Type"] = "application/json"
req.body = {}.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
cancel_session = JSON.parse(res.body) {
"object": "cancel_session",
"id": "ses_GMLvwQHvtdEcp5DZ",
"livemode": false,
"step": "done",
"outcome": "cancelled",
"locale": "en",
"holdout": false,
"published": true,
"customer": {
"id": "cst_Jy3PTUaA6DX1CQc3",
"stripe_customer_id": "cus_1",
"external_id": "user_42"
},
"subscription": {
"id": "nsub_MstpgaamkomcxhiZ",
"plan": null,
"status": "active",
"amount_cents": 4900,
"currency": "eur",
"interval": "month",
"interval_count": 1,
"current_period_end": "2026-11-24T10:00:00Z",
"cancel_at_period_end": true
},
"subscriptions": [],
"reasons": [
{
"key": "too_expensive",
"label": "It's too expensive",
"free_text": false
},
{
"key": "missing_feature",
"label": "A feature is missing",
"free_text": false
},
{
"key": "not_using",
"label": "I don't use it enough",
"free_text": false
},
{
"key": "switching",
"label": "I'm switching to another tool",
"free_text": false
},
{
"key": "temporary",
"label": "I only need a break",
"free_text": false
},
{
"key": "project_finished",
"label": "My project is finished",
"free_text": false
},
{
"key": "other",
"label": "Something else",
"free_text": true
}
],
"offer": null,
"talk_url": null,
"texts": {
"review_ask": "Would you leave a short review on {{platform}}? No reward, just your honest opinion.",
"review_cta": "Leave a review",
"title": "Before you go",
"reason_prompt": "Why are you cancelling?",
"other_placeholder": "Tell us more (optional)",
"offer_title": "One last thing",
"accept": "Accept the offer",
"decline": "No thanks, continue",
"feedback_prompt": "Anything we could have done better?",
"feedback_submit": "Continue",
"confirm_title": "Cancel your subscription?",
"confirm_body": "You keep access until the end of the paid period.",
"cancel_now": "Cancel my subscription",
"saved_title": "Thanks for staying!",
"cancelled_title": "Your subscription is cancelled",
"cancelled_body": "You keep access until the end of the paid period.",
"offer_discount": "{{percent}}% off for {{months}} months",
"offer_pause": "Pause for {{months}} months, free",
"offer_downgrade": "Switch to {{plan}}",
"offer_talk": "Talk to the team",
"choose_subscription_title": "Which subscription?",
"choose_subscription_body": "You have several subscriptions. Choose the one you want to cancel.",
"choose_subscription_select": "Continue with this one",
"cancel_this_one": "Cancel this one",
"renews_on": "Renews on {{date}}"
},
"feedback_enabled": true,
"feedback": {
"enabled": true,
"required": false
},
"theme": {
"object": "cancel_flow_theme",
"preset": "light",
"format": "modal",
"tokens": {
"bg": "#f5f6fa",
"surface": "#ffffff",
"text": "#1c1e4a",
"muted": "#686b85",
"accent": "#4637d9",
"accent_text": "#ffffff",
"border": "#e3e5ec",
"danger": "#c2410c",
"font": "system-ui, sans-serif",
"font_size": 15,
"radius": 12,
"radius_control": 8,
"overlay": 0.5,
"max_width": 480,
"shadow": "md",
"spacing": "regular",
"border_width": 1,
"border_style": "solid",
"font_scale": 1.25,
"font_weight_title": 600
},
"font_url": null,
"brand": "logo",
"logo_url": null,
"app_name": "Acme",
"end_screen": {
"style": "celebrate",
"auto_close_seconds": 6
},
"custom_css": "",
"cancel_button_check": "ok",
"classes": [
"nc-modal",
"nc-title",
"nc-reason",
"nc-offer",
"nc-btn-primary",
"nc-btn-cancel",
"nc-footer"
]
},
"cancel_url_now": null,
"cancel_status": "done",
"cancel_effective_at": "2026-11-24T10:00:00Z",
"result": null,
"access_until": "2026-11-24T10:00:00Z",
"confirmation_email_sent": false,
"review": null,
"expires_at": "2026-11-05T10:00:00Z",
"created_at": "2026-11-04T10:00:00Z"
} The cancel modal's design (tokens, format, logo, CSS)
The published version of the key's mode. Every token is present (preset, then the account's). For native apps and the widget; also inside each cancel session (with its segment / A/B design).
- Publishable key (
pk_test_…). A secret key works too.
PATH PARAMETERS
-
default or flw_…
RETURNS
-
200the theme — a CancelFlowTheme
curl https://api.nochurn.sh/v1/cancel_flows/flw_EmdD2FFD6aiqGNqz/theme \
-H "Authorization: Bearer pk_test_…"const res = await fetch("https://api.nochurn.sh/v1/cancel_flows/flw_EmdD2FFD6aiqGNqz/theme", {
headers: {
Authorization: "Bearer pk_test_…",
},
});
const cancelFlowTheme = await res.json();import requests
res = requests.get(
"https://api.nochurn.sh/v1/cancel_flows/flw_EmdD2FFD6aiqGNqz/theme",
headers={
"Authorization": "Bearer pk_test_…",
},
)
cancel_flow_theme = res.json()require "net/http"
require "json"
uri = URI("https://api.nochurn.sh/v1/cancel_flows/flw_EmdD2FFD6aiqGNqz/theme")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer pk_test_…"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
cancel_flow_theme = JSON.parse(res.body) {
"object": "cancel_flow_theme",
"preset": "dark",
"format": "panel",
"tokens": {
"bg": "#050505",
"surface": "#0b0b0c",
"text": "#ededed",
"muted": "#9b9ba1",
"accent": "#ededed",
"accent_text": "#050505",
"border": "#2a2a2e",
"danger": "#f97316",
"font": "Inter, sans-serif",
"font_size": 15,
"radius": 12,
"radius_control": 8,
"overlay": 0.6,
"max_width": 480,
"shadow": "none",
"spacing": "compact",
"border_width": 2,
"border_style": "dashed",
"font_scale": 1.5,
"font_weight_title": 500
},
"font_url": "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap",
"brand": "logo",
"logo_url": "https://cdn.acme.com/logo.svg",
"app_name": "Acme",
"end_screen": {
"style": "celebrate",
"auto_close_seconds": 6
},
"custom_css": "",
"cancel_button_check": "ok",
"classes": [
"nc-modal",
"nc-title",
"nc-reason",
"nc-offer",
"nc-btn-primary",
"nc-btn-cancel",
"nc-footer"
]
}