Quickstart
Wire up your retention engine in 10 minutes. Install the widget, point it at a token endpoint, and Unchurn handles eligibility, offer routing, Stripe mutations, verification, and recording end to end. This guide runs a real test-mode flow in your Next.js app. Different stack? Jump to Other setups.
Before you start
You need:
- An Unchurn account (sign up ). After signup you’ll find a merchant ID and a signing secret under Settings → Keys in the dashboard.
- A Stripe test-mode subscription you can target. Active, trialing, and past_due subscriptions all work. If you don’t have one yet, create one from the Stripe test dashboard .
- A Next.js (App Router) app with signed-in users. Any auth — NextAuth, Clerk, Supabase, custom. All Unchurn needs is the Stripe subscription ID of the signed-in user.
1. Install
pnpm add @unchurn.dev/widget@latest
# or: npm install @unchurn.dev/widget@latestThe alpha dist-tag tracks the current release.
2. Set environment variables
# .env.local
UNCHURN_SECRET=... # Settings → Keys in the dashboard
UNCHURN_MERCHANT_ID=mch_...Both are server-only — never prefix with NEXT_PUBLIC_. The secret is what signs every token; if it leaks, rotate it from the dashboard.
3. Add a server endpoint that signs tokens
Create one route. This file is where the secret lives and where you authenticate the request.
// 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: 'test', // change to 'live' when you're ready — see step 6
}
},
})This is where you decide who’s allowed to cancel what. Return null and the request is rejected; return a subscription ID and the handler issues a short-lived signed token scoped to that exact subscription. Nobody can spoof it from the browser.
If you don’t have auth wired yet but want to see the widget, skip to the CDN demo — it works unsigned in test mode.
4. Add the cancel button
// app/components/CancelButton.tsx
'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 component fetches a signed token from your route, loads the widget runtime, and opens the cancel flow. The appearance block is optional; defaults match neutral light and dark themes.
5. Try it
Click the button. You should see the cancel flow open as a centered modal with the feedback step.
If it doesn’t open, check three things in this order:
- The browser console. A red error here is usually the fastest path to the cause.
- The Network tab. You should see a
POST /api/unchurn/tokenfrom your app returning200, followed by a request toapi.unchurn.devalso returning200. If the token request returns401, yourresolveUserreturnednull— check the auth lookup. If it returns500, your environment variables aren’t loaded — Next.js requires a restart after editing.env.local. - The dashboard. Open Sessions in the Unchurn dashboard. A successful open creates one row tagged with this subscription.
Run the flow to an outcome — pick a reason, take or decline an offer, confirm. The session row updates with the outcome (saved, canceled, paused, plan-switched, trial-extended).
6. Switch to live mode
When you’re ready, flip the mode in resolveUser:
mode: process.env.NODE_ENV === 'production' ? 'live' : 'test',Live mode requires the subscription to be in your live Stripe account, and the merchant ID must match your live-mode merchant from the dashboard. See Test and live modes for the full picture.
Where to go next
Most teams go to the dashboard next — until you wire up your cancellation reasons and pick which offers to make, the flow shows defaults that won’t reflect your product. Start with Reasons and offers in the dashboard, then come back when you’re ready for Appearance and outcome callbacks.
Other setups
React, not Next.js
Same model: build a server endpoint that issues a signed token in whatever framework you use (Express, Hono, Fastify, Rails, Django) using the token format. Then render UnchurnTrigger or useUnchurn from @unchurn.dev/widget/react.
No bundler — plain JavaScript
import { createUnchurn } from '@unchurn.dev/widget'
const unchurn = createUnchurn({ tokenEndpoint: '/api/unchurn/token' })
document.querySelector('#cancel-button')?.addEventListener('click', () => {
void unchurn.open()
})CDN demo (no backend required)
Test mode only. Unsigned CDN calls are for local UI previews and design reviews — never use this in production.
<script src="https://cdn.unchurn.dev/widget.js" async></script>
<button id="cancel-btn">Cancel subscription</button>
<script>
document.getElementById('cancel-btn').addEventListener('click', () => {
window.unchurn.open({
merchantId: 'mch_YOUR_MERCHANT_ID',
subscriptionId: 'sub_STRIPE_TEST_SUB_ID',
mode: 'test',
})
})
</script>Substitute your merchant ID and a real Stripe test-mode subscription ID. No signed token needed in test mode — the backend accepts unsigned calls only when the mode is test.