Quickstart
Ship a cancel-flow widget in 6 lines of code. Works with Next.js App Router out of the box.
1. Install
pnpm add @unchurn.dev/widget
# or: npm install @unchurn.dev/widget2. Mint signed tokens server-side
Create one route file. This is where your merchant signing secret lives — never expose it to the browser.
// app/api/unchurn/token/route.ts
import { createUnchurnHandler } from '@unchurn.dev/widget/nextjs'
import { getCurrentUser } from '@/lib/auth' // your own auth helper
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) return null
return { subscriptionId: user.stripeSubscriptionId }
},
})3. Drop the widget into your app
// app/components/CancelButton.tsx
'use client'
import { useUnchurn } from '@unchurn.dev/widget/react'
export function CancelButton() {
const { show, isReady } = useUnchurn({ tokenEndpoint: '/api/unchurn/token' })
return (
<button disabled={!isReady} onClick={show}>
Cancel subscription
</button>
)
}That’s it. The widget fetches a signed token from your server and handles everything else.
What this does
- Your server minted a scoped HMAC token that looks like
unch_live_…. The prefix identifies the environment at a glance, the same convention Stripe uses forsk_live_…. - The widget called the Unchurn backend with that token; our API verified the signature against your secret before creating a session.
- No
subscription_idin a browser script can trigger a real Stripe mutation unless it carries a valid signed token.
Test mode
For development, set mode: 'test' in the handler options. Test-mode tokens are prefixed unch_test_ and can’t trigger live Stripe mutations — the backend rejects any attempt to mix the two.
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) return null
return {
subscriptionId: user.stripeSubscriptionId,
mode: 'test', // issue unch_test_… tokens for local dev
}
},
})Environment variables
# .env.local
UNCHURN_SECRET=… # from your merchant dashboard (32-byte hex)
UNCHURN_MERCHANT_ID=mch_…Without Next.js
If you aren’t on App Router, you can still mint tokens server-side. The primitives under @unchurn.dev/widget/nextjs — encodePayload + signPayload — are framework-agnostic and run on any Node 18+ runtime. See the auth tokens reference for the full signing protocol, token format, and error codes.
Test mode only — do not use in production: the no-auth CDN path
below lets you get the widget running with a single <script> tag, no server.
Any browser that guesses a subscription ID can open the flow for it. Use it
only for UI experiments and demos; flip to the signed flow above before going
live.
<!-- CDN demo — insecure; no HMAC verification -->
<script src="https://cdn.unchurn.dev/widget.js" async></script>
<script>
document.getElementById('cancel-btn').addEventListener('click', () => {
window.unchurn.showCancelFlow({
merchantId: 'mch_YOUR_MERCHANT_ID',
subscriptionId: 'sub_STRIPE_SUB_ID',
mode: 'test',
})
})
</script>Next steps
- Widget configuration — customize offers, copy, and branding
- Auth tokens — signing protocol, token format, error codes
- Test and live modes — switching between Stripe test and live mode