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_IDandUNCHURN_SECRETfrom Settings → Keys. - Connect your Stripe account (test and/or live). See Connect Stripe.
- Set
UNCHURN_SECRETandUNCHURN_MERCHANT_IDas server environment variables — never exposeUNCHURN_SECRETto the browser.
# .env.local
UNCHURN_SECRET=...
UNCHURN_MERCHANT_ID=mch_...npm install
npm install @unchurn.dev/widget@latest
# or
pnpm add @unchurn.dev/widget@latestThe @alpha tag tracks the current release.
The package has three entry points:
| Import | What it gives you | Where it runs |
|---|---|---|
@unchurn.dev/widget/react | <UnchurnTrigger>, useUnchurn | Browser, React 18+ |
@unchurn.dev/widget/server | createUnchurnHandler, mintUnchurnToken | Server only — any JS runtime |
@unchurn.dev/widget | createUnchurn — the framework-free factory | Browser, 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
- Quickstart — minimal Next.js install end-to-end in under 10 minutes.
- React guide — components, hook, custom triggers.
- Server integration — token routes for every framework, auth wiring, middleware.
- Vanilla guide —
createUnchurnand the CDN runtime. - Appearance — match the widget to your brand.