Docs navigation
Build a connected flow
Connected mode runs the SDK against a server-minted token. With session set, the SDK fetches the dashboard-configured flow from Churnkey and applies billing actions through your connected provider on accept. The component API is the same as open-source mode — you add session and appId.
Before you begin
- A Churnkey account with your billing provider connected (Stripe, Braintree, Chargebee, Paddle, Maxio). Without a connected provider, connected mode records sessions but can't apply billing actions.
- Your
appIdand an API key from the Churnkey dashboard. The API key never leaves your server.
Direct mode. The token alone is enough for server-driven flow config and session recording. You can mint a token without connecting a provider and pass
customer+subscriptionsdirectly on the component; billing actions fall back to yourhandle<Type>callbacks. See Server-side tokens.
1. Install the server SDK
npm install @churnkey/node@churnkey/node has no runtime dependencies. Token signing is a local HMAC computation, so minting doesn't hit the network.
2. Mint a token in an authenticated route
// server.ts
import { Churnkey } from '@churnkey/node'
import express from 'express'
const ck = new Churnkey({
appId: process.env.CHURNKEY_APP_ID!,
apiKey: process.env.CHURNKEY_API_KEY!,
})
const app = express()
app.post('/api/cancel-flow/session', requireAuth, async (req, res) => {
const token = ck.createToken({
customerId: req.user.customerId,
subscriptionId: req.user.subscriptionId, // optional
mode: process.env.NODE_ENV === 'production' ? 'live' : 'test',
})
res.json({ token })
})The token is a short-lived signed credential. Churnkey trusts the signed customerId over anything the client sends, so re-authenticate before issuing one. Treat the token as a bearer credential — anyone who holds it can take billing actions on the customer's behalf until it expires.
3. Fetch the token on demand from the client
import { useState } from 'react'
import { CancelFlow } from '@churnkey/react'
import '@churnkey/react/styles.css'
export function CancelButton() {
const [token, setToken] = useState<string | null>(null)
const [open, setOpen] = useState(false)
async function startCancel() {
const res = await fetch('/api/cancel-flow/session', { method: 'POST' })
const { token } = await res.json()
setToken(token)
setOpen(true)
}
return (
<>
<button onClick={startCancel}>Cancel subscription</button>
{open && token && (
<CancelFlow
appId="app_xxx"
session={token}
onClose={() => setOpen(false)}
/>
)}
</>
)
}The SDK fetches the flow config from Churnkey, renders it, and applies accepted offers through your connected provider.
4. Pass customer attributes for segmentation (optional)
Your app often knows things about the customer that the billing provider doesn't: usage counts, entitlements, lifecycle stage. Pass them as customerAttributes and segments can match on them when Churnkey picks the blueprint.
<CancelFlow
appId="app_xxx"
session={token}
customerAttributes={{ videosCreated: 28 }}
/>Churnkey still fetches the real customer from your connected provider; the attributes are layered on top for segment evaluation. So a segment targeting videosCreated greater than 20 can route power users to a flow with more generous offers. The same keys also resolve as merge fields in dashboard copy and are recorded on the session.
Define the attribute in the dashboard segment builder before filtering on it.
5. Keep one action client-side (optional)
Defining a handle<Type> callback opts that offer type out of Churnkey's server action. Use it when your billing action needs logic that has to stay in your code — custom proration, writes to sibling systems, audit-log requirements.
<CancelFlow
appId="app_xxx"
session={token}
// Churnkey applies discount, pause, trial_extension via the connected provider
// (no handlers defined for those)
handlePlanChange={async (offer, customer) => {
// Our plan change has custom proration logic. Keep it.
await myBilling.changePlan({
customerId: customer?.id,
planId: offer.result.planId,
proration: 'create_invoice',
})
}}
onAccept={(offer) => analytics.track('offer_accepted', { type: offer.type })}
onCancel={() => router.push('/goodbye')}
/>The decision is per offer type: define a handler for any offer you want to own, and the rest stay with Churnkey.
Next steps
- Server-side tokens — token minting and Direct mode in detail.
- Connecting billing providers — Stripe, Braintree, Chargebee, Paddle, Maxio setup.