API REFERENCE v1

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

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
  • subscriptions array of objects

    At choose_subscription only (several live subscriptions, none named by the token's subscription claim): the customer picks one (answer { subscription }) or cancels one (cancel { subscription }). Empty otherwise.

    Show child attributesHide child attributes
  • reasons array of objects
    Show child attributesHide child attributes
  • 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
  • Same as feedback.enabled (kept for older widgets).

  • feedback object
    Show child attributesHide child attributes
    • enabled boolean

      The feedback step is shown.

    • required boolean

      "Continue" needs a text (400 otherwise). Cancelling never does.

  • 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
    • 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
      • auto_close_seconds integer | null 3 to 30

        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).

      • 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
  • cancel_effective_at string | null date-time
  • 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.

    • next_invoice_at string | null date-time

      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.

    • discounted_until string | null date-time

      discount: when the usual price comes back (acceptance + the offer's months).

    • discounted_invoices array of objects 12 items at most

      discount: the invoices due before discounted_until (12 at most), at next_invoice_amount_cents. [] otherwise.

      Show child attributesHide child attributes
    • resumes_at string | null date-time

      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.

  • access_until string | null date-time

    Cancelled: access lasts until then (cancel_effective_at: the period end, or now when immediate). Null otherwise, and while the cancellation is pending.

  • 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.

    Show child attributesHide child attributes
    • platform enum
      • g2
      • capterra
      • trustpilot
      • google
      • app_store
      • product_hunt
    • url string
    • text string
    • cta string
  • expires_at string date-time
  • created_at string date-time

Schema: CancelSession

THE CANCEL SESSION OBJECT
{
  "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 the customer instead of a token.

HEADER PARAMETERS

BODY PARAMETERS

  • customer string
  • subscription string

    Secret key only (from a browser: the token's subscription claim). 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

ERRORS

  • 401 unauthorized a publishable key without a customer token
  • 403 forbidden an origin that is not allowed
POST/v1/cancel_sessions
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)
RESPONSE 201
{
  "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 the customer instead of a token.

PATH PARAMETERS

  • id string REQUIRED

HEADER PARAMETERS

RETURNS

ERRORS

  • 404 not_found a session of another customer
GET/v1/cancel_sessions/{id}
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)
RESPONSE 200
{
  "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

  • id string REQUIRED

    default or flw_…

QUERY PARAMETERS

RETURNS

  • 200 texts in the customer's language, reasons, theme — a CancelFlow
GET/v1/cancel_flows/{id}
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)
RESPONSE 200
{
  "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 the customer instead of a token.

PATH PARAMETERS

  • id string REQUIRED

HEADER PARAMETERS

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.

  • comment string 2000 characters at most

    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 string 2000 characters at most

    Feedback step: optional unless feedback.required.

RETURNS

ERRORS

POST/v1/cancel_sessions/{id}/answer
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)
RESPONSE 200
{
  "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 the customer instead of a token.

PATH PARAMETERS

  • id string REQUIRED

HEADER PARAMETERS

BODY PARAMETERS

  • subscription string

    choose_subscription step: the one to cancel (nsub_…, sub_… or its id in your billing).

RETURNS

ERRORS

POST/v1/cancel_sessions/{id}/cancel
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)
RESPONSE 200
{
  "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

  • id string REQUIRED

    default or flw_…

RETURNS

GET/v1/cancel_flows/{id}/theme
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)
RESPONSE 200
{
  "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"
  ]
}