Skip to Content
WidgetInstallation

Installation

Get a Cancel button into your app that opens the retention flow. There are two ways to install: the npm package if you have a build step, the CDN script tag if you don’t. Both load the same hosted runtime, render the same UI, and call the same backend — they differ only in how you import them.

Use the npm package if your app has a bundler (Next.js, Vite, Webpack, etc.) and you can write a small server endpoint. This is the typical path.

Use the CDN script tag if you have a no-build site, you’re embedding the widget into a Webflow-style page, or you’re prototyping. Production CDN installs still need a server endpoint to sign tokens.

Prerequisites

Before you install:

  • Sign up at the Unchurn dashboard. Grab UNCHURN_MERCHANT_ID and UNCHURN_SECRET from Settings → Keys.
  • Connect your Stripe account (test and/or live). See Connect Stripe.
  • Set UNCHURN_SECRET and UNCHURN_MERCHANT_ID as server environment variables — never expose UNCHURN_SECRET to the browser.
# .env.local UNCHURN_SECRET=... UNCHURN_MERCHANT_ID=mch_...

npm install

npm install @unchurn.dev/widget@latest # or pnpm add @unchurn.dev/widget@latest

The @alpha tag tracks the current release.

The package has three entry points:

ImportWhat it gives youWhere it runs
@unchurn.dev/widget/react<UnchurnTrigger>, useUnchurnBrowser, React 18+
@unchurn.dev/widget/servercreateUnchurnHandler, mintUnchurnTokenServer only — any JS runtime
@unchurn.dev/widgetcreateUnchurn — the framework-free factoryBrowser, any framework

Most React apps need only the first two. The third is for non-React bundled apps (Vue, Svelte, Solid, vanilla TypeScript, etc.).

Fetch-API server (Next.js App Router, Remix, SvelteKit, Hono, Workers, Edge, Bun, Deno)

Use createUnchurnHandler — it returns a (Request) => Promise<Response> you can mount in any Fetch-API framework. Example: Next.js App Router.

// app/api/unchurn/token/route.ts import { createUnchurnHandler } from '@unchurn.dev/widget/server' import { getCurrentUser } from '@/lib/auth' export const POST = createUnchurnHandler({ secret: process.env.UNCHURN_SECRET!, merchantId: process.env.UNCHURN_MERCHANT_ID!, resolveUser: async (req) => { const user = await getCurrentUser(req) if (!user?.stripeSubscriptionId) return null return { subscriptionId: user.stripeSubscriptionId, mode: process.env.NODE_ENV === 'production' ? 'live' : 'test', } }, })

Node-http server (Pages Router, Express, Fastify, Koa, Lambda)

Use mintUnchurnToken — a pure function. Write a thin handler that reads auth and returns the JSON. Example: Express.

import express from 'express' import { mintUnchurnToken } from '@unchurn.dev/widget/server' const router = express.Router() router.post('/api/unchurn/token', async (req, res) => { const user = req.session?.user if (!user?.stripeSubscriptionId) return res.status(401).end() const token = mintUnchurnToken({ secret: process.env.UNCHURN_SECRET!, merchantId: process.env.UNCHURN_MERCHANT_ID!, subscriptionId: user.stripeSubscriptionId, mode: process.env.NODE_ENV === 'production' ? 'live' : 'test', }) res.json(token) })

Pages Router, Fastify, Koa, and Lambda follow the same shape — see Server integration for each.

React client

The client half is identical no matter which server framework you used. Drop in the trigger:

'use client' import { UnchurnTrigger } from '@unchurn.dev/widget/react' export function CancelButton() { return ( <UnchurnTrigger tokenEndpoint="/api/unchurn/token" appearance={{ variables: { colorPrimary: '#1942F5', colorPrimaryForeground: '#ffffff', fontFamily: 'inherit', }, }} > Cancel subscription </UnchurnTrigger> ) }

The signed token expires in 600 seconds. That’s the whole install. See Server integration for every framework’s token route, the React guide for useUnchurn and custom triggers, and Appearance for theming.

Non-React bundlers

For Vue, Svelte, Solid, or plain TypeScript:

import { createUnchurn } from '@unchurn.dev/widget' const unchurn = createUnchurn({ tokenEndpoint: '/api/unchurn/token', defaultFlow: { appearance: { variables: { colorPrimary: '#1942F5', colorPrimaryForeground: '#ffffff', fontFamily: 'inherit', }, }, }, }) document.querySelector('#cancel-button')?.addEventListener('click', () => { void unchurn.open() })

createUnchurn loads the runtime, fetches a fresh signed token on each open, and opens the flow. See Vanilla guide for the full walk-through.

CDN install

Drop this into the <head>:

<script src="https://cdn.unchurn.dev/widget.js" async></script>

For production, fetch a signed token from your server before opening the flow:

<button id="cancel-btn">Cancel subscription</button> <script> document.getElementById('cancel-btn').addEventListener('click', async () => { const res = await fetch('/api/unchurn/token', { method: 'POST', credentials: 'same-origin', }) if (!res.ok) return const token = await res.json() window.unchurn.open({ merchantId: token.merchantId, subscriptionId: token.subscriptionId, authToken: token.authToken, mode: token.mode, }) }) </script>

For test-mode previews you can skip the server token by calling window.unchurn.open({ merchantId, subscriptionId, mode: 'test' }) directly. Never ship that to production — anyone could trigger a cancel flow for any subscription.

Content Security Policy

If your app sets a CSP, allow the runtime and API:

Content-Security-Policy: script-src 'self' https://cdn.unchurn.dev; connect-src 'self' https://app.unchurn.dev; style-src 'self' 'unsafe-inline';

The widget injects its stylesheet into the page. If your CSP blocks inline styles, add a nonce or 'unsafe-inline' for the widget origin.

Where to go next