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 })
})
FieldRequiredNotes
customerIdyesThe customer's ID in your billing provider, or your internal ID for Direct mode.
subscriptionIdnoThe specific subscription to cancel. Omit when the customer has one obvious subscription.
modeno'live' or 'test'. Defaults to 'live'. The mode is signed into the token, so the client can't override it.
expiresInnoToken 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

PropertyBehavior
SignatureHMAC-SHA256 with your API key. The key never leaves your server.
ScopeOne customer, one cancel flow session. Don't reuse tokens across sessions.
Lifetime30 minutes by default. Customize per request with expiresIn.
Mode encodingmode is signed into the token. A client-side mode prop can't override it.
RevocationNo 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