Docs navigation
Server-side tokens
The session token is a short-lived signed credential proving the cancelling customer's ID came from your server. The SDK uses it to fetch the dashboard-configured flow and apply billing actions against the connected provider.
Anyone can edit a customer.id prop in the browser, so the client SDK can't be the source of truth for which customer is cancelling. Signing the token with your API key on the server lets Churnkey verify the customer ID came from you.
Install the server SDK
npm install @churnkey/node@churnkey/node has zero runtime dependencies. Token signing is a local HMAC computation, so minting doesn't hit the network.
Mint a token
import { Churnkey } from '@churnkey/node'
const ck = new Churnkey({
appId: process.env.CHURNKEY_APP_ID!,
apiKey: process.env.CHURNKEY_API_KEY!,
})
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 })
})| Field | Required | Notes |
|---|---|---|
customerId | yes | The customer's ID in your billing provider, or your internal ID for Direct mode. |
subscriptionId | no | The specific subscription to cancel. Omit when the customer has one obvious subscription. |
mode | no | 'live' or 'test'. Defaults to 'live'. The mode is signed into the token, so the client can't override it. |
expiresIn | no | Token lifetime in seconds. Defaults to 1800 (30 minutes). |
customerId must match the format your connected billing provider uses, since Churnkey looks it up there to find the subscription. expiresIn is a security/usability trade-off — shorter is safer if a token is stolen, longer is friendlier if the customer is slow to complete the flow. Thirty minutes works for most cases.
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)}
/>
)}
</>
)
}Mint the token on click, not at page load. A token sitting in client state for hours has a longer attack window than one minted just before use. Minting costs one HMAC computation — the lazy path is cheap.
Direct mode: customer data alongside the token
Connected billing providers (Stripe, Braintree, Chargebee, Paddle, Maxio) let Churnkey look up the customer's subscription details server-side. If your customer or subscription data lives outside any of those providers, pass it directly on the component.
<CancelFlow
appId="app_xxx"
session={token}
customer={{
id: 'cus_123',
email: 'jane@acme.com',
name: 'Jane',
metadata: { plan: 'pro', accountAge: '2y' },
}}
subscriptions={[{
id: 'sub_456',
start: '2024-06-01',
status: {
name: 'active',
currentPeriod: { start: '2025-04-01', end: '2025-05-01' },
},
items: [{
price: {
id: 'price_pro',
amount: { value: 2999, currency: 'usd' },
},
}],
}]}
/>The token's signed customerId is the source of truth — Churnkey uses it to key the session and check authorization. The body fills in details the token doesn't carry: custom metadata, current plan price, tax-relevant addresses. If both reference the same field, the token wins. See DirectCustomer and DirectSubscription for the full schemas.
Token mechanics
| Property | Behavior |
|---|---|
| Signature | HMAC-SHA256 with your API key. The key never leaves your server. |
| Scope | One customer, one cancel flow session. Don't reuse tokens across sessions. |
| Lifetime | 30 minutes by default. Customize per request with expiresIn. |
| Mode encoding | mode is signed into the token. A client-side mode prop can't override it. |
| Revocation | No live revocation API. Tokens expire on their own. |
There's no live revocation API by design — it would require a database lookup on every token use, adding latency to every flow. Use short lifetimes (5–30 minutes) for sensitive accounts instead.
Next steps
- Connecting billing providers — Stripe, Braintree, Chargebee, Paddle, Maxio.