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 appId and 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 + subscriptions directly on the component; billing actions fall back to your handle<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. 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