Integration docs

Add churn prevention to your subscription app.

Connect Stripe, add the cancel flow widget, recover failed payments, and send the customer signals Churnless needs to protect MRR.

Typical setup time
5 min
Create Churnless and Stripe keys
15 min
Place the cancel flow in billing settings
20 min
Add payment wall and product signal events
01

Create keys

Use a browser-safe publishable key in your app and a secret key only from your server.

02

Connect Stripe

Paste a restricted Stripe key so Churnless can read products, customers, subscriptions, invoices, and payment failures.

03

Add the cancel flow

Mint a customer token on your server, then open the script-tag widget where cancellation starts.

04

Send signals

Track usage, feedback, support, and lifecycle events so the save path can adapt to the customer.

Step 1

Create the right keys.

Churnless uses two key types. The publishable key identifies your installation in the browser; customer billing actions also require a signed customer token. The secret key is only for server-side API calls, MCP, jobs, and customer imports.

Publishable key

Use `fk_pub_...` with the tracking pixel and session recorder. On its own it cannot start a cancel flow or read payment state: those also need a short-lived customer token minted by your server.

Secret key

Use `fk_live_...` only on your server, including to mint customer tokens. Never expose it to client bundles, mobile apps, or public config. Test-mode Churnless keys are not available yet; use a Stripe test-mode key while you integrate.

Stripe restricted key

In Churnless settings, paste a restricted Stripe key with the prefilled least-privilege permissions. Churnless uses read scopes to import customers, products, prices, subscriptions, invoices, and charges, plus limited write scopes for save actions such as pauses, discounts, and cancellation at period end. Start in test mode, run the import, then switch to your live Stripe key when the mapping looks right.

Step 2

Mint customer tokens on your server.

Cancel flows and payment status are authorized by a short-lived customer token, not by a key in the browser. Your backend decides who the customer is; Churnless signs a token bound to that customer and subscription.

  1. Your backend authenticates the signed-in user and resolves their Stripe customer ID and the exact subscription they are managing. Never accept those IDs from the browser.
  2. It calls POST /api/v1/customer-session with your secret key and a purpose: cancel_flow (requires externalSubscriptionId) or payment_status.
  3. Churnless returns a signed customerToken that expires after 15 minutes. Your frontend passes it to the widget through getCustomerToken, which is called each time a flow opens.
  4. A started cancel session stays valid for 60 minutes of inactivity, up to 4 hours in total.

Enforce your app’s CSRF protection and billing-management permissions on this endpoint. For accounts with several subscriptions, resolve the selected subscription against the signed-in account. Keep a direct cancellation path available in case Churnless cannot start.

Your authenticated backend
ts
// Your app: app/api/churnless/customer-session/route.ts
// Adapt this import to your existing authenticated billing-session resolver.
import { requireBillingSession } from "@/lib/billing-session";

export async function POST(request: Request) {
  // Enforce your app's same-origin/CSRF policy and billing-management role.
  const billing = await requireBillingSession(request);
  const { purpose } = await request.json();
  if (!["cancel_flow", "payment_status"].includes(purpose)) {
    return Response.json({ error: "Invalid purpose" }, { status: 400 });
  }
  const result = await fetch("https://www.getchurnless.com/api/v1/customer-session", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-API-Key": process.env.CHURNLESS_API_KEY!,
    },
    body: JSON.stringify({
      purpose,
      customerId: billing.stripeCustomerId,
      externalSubscriptionId: billing.stripeSubscriptionId,
      productId: process.env.CHURNLESS_PRODUCT_ID,
    }),
    cache: "no-store",
  });
  return new Response(await result.text(), {
    status: result.status,
    headers: { "Content-Type": "application/json", "Cache-Control": "no-store" },
  });
}
Step 3

Add the cancel flow widget.

A single script tag works with any stack. The widget opens as an accessible modal, always shows a direct cancel option, and hands off to your fallback URL if it cannot finish.

Cancel widget
html
<script src="https://www.getchurnless.com/widget/v1.js"></script>
<script>
  const cancelFlow = window.Churnless?.init({
    customerId: "cus_123", // the signed-in customer's Stripe ID
    // Returns the customerToken string from your backend endpoint below.
    getCustomerToken: async (purpose) => {
      const response = await fetch("/api/churnless/customer-session", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ purpose }),
      });
      if (!response.ok) throw new Error("Could not authorize billing session");
      return (await response.json()).customerToken;
    },
    // Where customers can still cancel if the flow cannot start.
    fallbackCancelUrl: "/billing/cancel",
    // Called when anything blocks cancelling; send them to your own path.
    onUnavailable: () => window.location.assign("/billing/cancel"),
    onSaved: () => window.location.reload(),
    onCanceled: () => window.location.assign("/goodbye"),
  });

  document.getElementById("cancel-subscription").onclick = () => {
    // Ad blockers or a slow network can stop the script from loading:
    // never leave the customer without a way to cancel.
    if (!window.Churnless) {
      window.location.assign("/billing/cancel");
      return;
    }
    cancelFlow?.show();
  };
</script>
Upgrading

Migrating an existing widget embed.

Older embeds that only passed a publishable key and customerId keep working, but they skip the retention flow until you add a customer token.

A signed customer token is required

Mint a short-lived token on your backend (POST /api/v1/customer-session with your secret key) and pass getCustomerToken. With a callback the widget restarts an expired session once, transparently; a static customerToken cannot be refreshed. Without a token, init() logs one console error and hands off to your fallback.

fallbackCancelUrl and onUnavailable

onUnavailable fires once per flow for anything that blocks cancelling: the flow could not start, a request timed out (10 seconds), the session expired and could not restart, or the server could not confirm a cancellation. Without a handler the widget shows a Cancel subscription link to fallbackCancelUrl. Guard for a blocked script (if (!window.Churnless)) the same way.

Analytics: eventId and flowInstanceId

onTrackEvent payloads no longer include sessionId, because it authorizes billing actions. Join events on flowInstanceId (stable for one open flow) and deduplicate on eventId. Keep my subscription and Never mind now record outcome deflected instead of abandoned.

theme.mode

theme.mode is 'light', 'dark' or 'auto' (the default, which follows the customer's system setting). Set 'light' if your product has no dark theme.

Private beta

React SDK.

The React SDK wraps the same token flow in components: ChurnlessProvider, CancelFlow, and PaymentRecoveryWall. It is not yet published to npm. Email support@getchurnless.com to join the beta; until then, use the script-tag widget above.

Coming soon

The install command below will work once the package is published. It fails today.

Install (not yet available)
bash
# Coming soon (not yet published to npm)
npm install @churnless/react
Provider (beta)
tsx
"use client";

import { ChurnlessProvider } from "@churnless/react";

export function AppProviders({ children, customer }) {
  return (
    <ChurnlessProvider
      publishableKey={process.env.NEXT_PUBLIC_CHURNLESS_KEY}
      customerId={customer.stripeCustomerId}
      productId="prod_adscreator"
      trackingConsent={customer.analyticsConsent}
      getCustomerToken={async (purpose) => {
        const response = await fetch("/api/churnless/customer-session", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ purpose }),
        });
        if (!response.ok) throw new Error("Could not authorize billing session");
        return (await response.json()).customerToken;
      }}
    >
      {children}
    </ChurnlessProvider>
  );
}
React SDK (beta)

Replace the raw cancel button in React.

Pass the same customer ID you use in Stripe. Churnless starts a session, asks for the reason, chooses the next best step, and reports the outcome.

Cancel flow
tsx
"use client";

import { CancelFlow } from "@churnless/react";

export function BillingSettings({ customer }) {
  return (
    <CancelFlow
      customerId={customer.stripeCustomerId}
      onSaved={() => window.location.reload()}
      onCanceled={() => window.location.assign("/goodbye")}
      onError={(error) => console.error(error)}
    >
      {({ show, isLoading }) => (
        <button onClick={show} disabled={isLoading}>
          {isLoading ? "Loading..." : "Cancel subscription"}
        </button>
      )}
    </CancelFlow>
  );
}
Step 4

Block involuntary churn without blocking recovery.

Dunning emails include a secure link to a hosted card-update page, with no code required. In React apps, the beta PaymentRecoveryWall shows customers with unresolved failures a clear path to update payment details; everyone else sees the app.

Payment recovery wall (React SDK beta)
tsx
import { PaymentRecoveryWall } from "@churnless/react";

export function ProtectedApp({ customer, children }) {
  return (
    <PaymentRecoveryWall
      customerId={customer.stripeCustomerId}
      fallback={children}
      pollInterval={60000}
    />
  );
}
Step 5

Send the product signals that predict churn.

The better the signal history, the better the cancellation path. Track meaningful usage, activation, support friction, billing friction, and feedback.

Browser pixel
html
<script>
  window.fk = window.fk || function(){(window.fk.q = window.fk.q || []).push(arguments)};
  fk("init", "fk_pub_...", {
    customerId: "cus_123",
    productId: "prod_adscreator",
    consent: true
  });
  fk("track", "feature_used", {
    feature: "campaign_export",
    value: 1
  });
</script>
<script async src="https://www.getchurnless.com/pixel/v1.js"></script>
Server event
ts
await fetch("https://www.getchurnless.com/api/v1/events", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-API-Key": process.env.CHURNLESS_API_KEY
  },
  body: JSON.stringify({
    customerId: "cus_123",
    productId: "prod_adscreator",
    eventType: "feature_used",
    eventName: "campaign_exported",
    category: "usage",
    payload: {
      exportCount: 3,
      campaignId: "camp_789"
    }
  })
});

Consent-first sites

Pass requireConsent: true to fk("init") and the pixel captures nothing until fk("consent", true). fk("consent", false) stops capture again. In the React SDK, set requireConsent on the provider and pass trackingConsent once your consent manager says yes.

Consent-first pixel
html
<script>
  window.fk = window.fk || function(){(window.fk.q = window.fk.q || []).push(arguments)};
  // Consent-first: nothing is captured until consent is granted.
  fk("init", "fk_pub_...", { customerId: "cus_123", requireConsent: true });
  // Call this from your consent manager. A call made before init finishes
  // loading is still honored.
  onAnalyticsConsent((granted) => fk("consent", granted));
</script>

Usage

feature_used, project_created, export_completed

Lifecycle

trial_started, activation_completed, team_invited

Friction

support_ticket_opened, error_seen, downgrade_viewed

Named product actions for session proof

For Friction Radar product issues, emit allowlisted action names so the AI PM can cite product intent (not only rage clicks). Prefer churnless_action tags in recording, or track with stable names like billing.portal_open, checkout.submit, onboarding.invite_sent, and error.payment_failed. Names must match ^[a-zA-Z][a-zA-Z0-9_.:-]{0,99}$. Settings → Products shows your instrumentation score.

Optional

Replay consented journeys without exposing sensitive data.

Session recording is available on every plan, but it is disabled for every product until you enable it in Settings → Products. The existing trackingConsent value remains the only end-user consent control.

Privacy modes

Balanced masks every form value while keeping ordinary page text for journey context. With the separate prompt-capture opt-in enabled in recording settings, recognized prompt and composer values are recorded verbatim; sensitive fields stay masked regardless.

Strict masks all form values and ordinary page text.

Both modes exclude canvas pixels, inline images, fonts, console output, and cross-origin iframe contents.

Mark sensitive regions

Add data-churnless-prompt to a prompt composer that should be readable in Balanced replay when prompt capture is opted in. Use rr-mask, rr-block, and rr-ignore, or the equivalent data-churnless-mask, data-churnless-block, and data-churnless-ignore attributes, to override capture for sensitive areas.

Sampling is deterministic per session. The recorder downloads only after product enablement, analytics consent, Do Not Track, and sampling checks pass. Replays stay private and expire after 30 days.

Advanced

Use the API and MCP server for automation.

Server-side integrations can import customers, push events, inspect health scores, list cancel sessions, read payment-recovery performance (each recovered payment credited to one method, plus what Churnless added against a no-treatment holdout), pause or resume recovery campaigns, and let internal AI tools query churn context.

Reference docs

OpenAPI reference

Browse every REST endpoint, schema, and request example in the generated API reference.

The endpoints this guide installs (customer sessions, cancel flows, payment status, identify, events, signals, recovery links, billing contacts, GDPR) work on every plan. The rest of the data API needs API access, which the Launch plan doesn’t include; those calls return 402. Connecting an AI agent over MCP with OAuth works on every plan.

Connect an AI agent over MCP

Add the hosted MCP server to your AI client and sign in with your Churnless account. You choose the workspace and whether the agent may work your Today inbox. Once connected, it can read customers, churn risk, reasons, cancel flows, payment recovery and alerts, then review what needs you today and approve, reject, snooze or clear items. Every change asks for your confirmation first, runs as you, and shows up in the agent ledger, where it can be undone. Works on every plan while it’s active. Disconnect apps under Settings → Developers → Connected AI apps.

Connect with OAuth
bash
# Claude Code (Claude, ChatGPT and Cursor: add the same URL as a remote MCP server)
claude mcp add --transport http churnless https://www.getchurnless.com/api/mcp

Server-to-server automation can send Authorization: Bearer fk_live_… to the same URL instead (needs API access).

Integration checklist

Ready to test with a real customer?

Create keys, connect Stripe in test mode, add the customer token endpoint and the widget, open the cancel flow from your billing page, and send at least one usage event for the same customer ID.