---
title: Quick Start Guide
description: A 3-step guide on how to implement Cancel Flows in your website
---

## Overview

Integrating Churnkey Cancel Flows into your application involves three main steps:

1. **Load the Churnkey script** - Add the Churnkey JavaScript library to your website to make `window.churnkey` available
2. **Generate secure authentication** - Create a backend endpoint that generates an HMAC hash to verify customer identity
3. **Initialize the cancel flow** - When a customer clicks cancel, fetch the authHash from your backend and display the Churnkey modal

This integration pattern ensures that only authenticated customers can access their cancellation options, protecting your application from unauthorized access.

## Step One: Place Script Element

The following code will pull in the Churnkey client-side module and add it under the `window.churnkey` namespace so that you can later initialize the Churnkey Cancel Flow for your customers. Place it in the HTML `<head>` element.
To find `YOUR_APP_ID`,from any initial page in Churnkey's dashboard you can navigate:
1. Settings (Bottom Left Corner)
2. Organization (Top Menu)
3. Scroll Down to the section Cancel Flow API Keys

<p align="center">
  <img src="./img/cancel_flow/cancel_flow_api_keys.png" alt="Cancel Flow APP_ID and API_KEY">
</p>


```javascript
<script>
!function(){
  if (!window.churnkey || !window.churnkey.created) {
    window.churnkey = { created: true };
    const a = document.createElement('script');
    a.src = 'https://assets.churnkey.co/js/app.js?appId=YOUR_APP_ID';
    a.async = true;
    const b = document.getElementsByTagName('script')[0];
    b.parentNode.insertBefore(a, b);
  }
}();
</script>
```

## Step Two: Generate Secure HMAC Hash

::alert{type="warning" :icon="/icon/paddle-logo.webp"}
**Note for Paddle Users**

Use the Subscription ID instead of Customer ID for creating the HMAC hash

::

To ensure that all customer requests processed by Churnkey are authorized, server-side verification is implemented. This involves generating an HMAC hash on the customer ID (or subscription ID for Paddle users) using SHA-256 hashing. Before triggering the Churnkey flow, a request is sent to the server to (a) validate the request's authenticity, typically using existing authorization measures, and (b) compute the customer's ID hash. Below are examples in various backend languages.

To find your `API_KEY`, from any initial page in Churnkey's dashboard you can navigate:
1. Settings (Bottom Left Corner)
2. Organization (Top Menu)
3. Scroll Down to the section Cancel Flow API Keys


::code-group
```js [Node.js]
const crypto = require("crypto");
const userHash = crypto.createHmac(
  "sha256",
  API_KEY // Your Churnkey API Key (keep this safe)
).update(CUSTOMER_ID).digest("hex"); // Send to front-end
```

```ts [Next.js]
import crypto from "crypto";
import type { NextApiRequest, NextApiResponse } from "next";

type Data = {
  userHash?: string;
  error?: string;
};

export default function handler(
  req: NextApiRequest,
  res: NextApiResponse<Data>
) {
  const { customerId } = req.body;

  if (!customerId) {
    return res.status(400).json({ error: "Missing customerId" });
  }

  const API_KEY = process.env.CHURNKEY_API_KEY || ""; // Your Churnkey API Key
  const userHash = crypto
    .createHmac("sha256", API_KEY)
    .update(customerId)
    .digest("hex");

  return res.status(200).json({ userHash });
}
```

```python [Python (Django)]
import hmac
import hashlib

user_hash = hmac.new(
    API_KEY, # Your Churnkey API Key (keep safe)
    CUSTOMER_ID, # Stripe Customer ID
    digestmod=hashlib.sha256
).hexdigest()
# Send user_hash to front-end
```

```ruby [Ruby (Rails)]
user_hash = OpenSSL::HMAC.hexdigest(
  "sha256",
  API_KEY, # Your Churnkey API Key (keep safe)
  CUSTOMER_ID # Stripe Customer ID
)
# Send user_hash to front-end
```

```php [PHP]
<?php
$user_hash = hash_hmac('sha256', CUSTOMER_ID, API_KEY);
echo $user_hash; // Send to front-end
?>
```

```go [GO]
package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
)

func main() {
	h := hmac.New(sha256.New, API_KEY) // Your Churnkey API Key (keep safe)
	h.Write(CUSTOMER_ID)                // Stripe Customer ID
	userHash := hex.EncodeToString(h.Sum(nil))
	// Send userHash to front-end
}
```

```java [Java]
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

public class Test {
    public static void main(String[] args) {
        try {
            String secret = API_KEY; // Your Churnkey API Key (keep safe)
            String message = CUSTOMER_ID; // Stripe Customer ID

            Mac sha256HMAC = Mac.getInstance("HmacSHA256");
            SecretKeySpec secretKey = new SecretKeySpec(secret.getBytes(), "HmacSHA256");
            sha256HMAC.init(secretKey);

            byte[] hash = sha256HMAC.doFinal(message.getBytes());
            StringBuffer userHash = new StringBuffer();
            for (byte b : hash) {
                userHash.append(String.format("%02x", b));
            }
            System.out.println(userHash.toString()); // Send to front-end
        } catch (Exception e) {
            System.out.println("Error: " + e.getMessage());
        }
    }
}
```

```csharp [C#]
using System;
using System.Security.Cryptography;
using System.Text;

public class Test
{
    public static void Main(string[] args)
    {
        try
        {
            string secret = API_KEY; // Your Churnkey API Key (keep safe)
            string message = CUSTOMER_ID; // Stripe Customer ID

            using (var hmacsha256 = new HMACSHA256(Encoding.UTF8.GetBytes(secret)))
            {
                byte[] hash = hmacsha256.ComputeHash(Encoding.UTF8.GetBytes(message));
                StringBuilder userHash = new StringBuilder();
                foreach (byte b in hash)
                {
                    userHash.Append(b.ToString("x2"));
                }
                Console.WriteLine(userHash.ToString()); // Send to front-end
            }
        }
        catch (Exception e)
        {
            Console.WriteLine("Error: " + e.Message);
        }
    }
}
```
::

## Step Three: Launch Churnkey

Once the HMAC hash has been generated, you can initialize and display the Churnkey Cancel Flow by calling `window.churnkey.init('show')` with the required configuration parameters.

```javascript
window.churnkey.init('show', {
  customerId: 'CUSTOMER_ID', // required unless Paddle
  authHash: 'HMAC_HASH', // required - fetched from your backend
  subscriptionId: 'SUBSCRIPTION_ID', // recommended unless Paddle
  appId: 'YOUR_APP_ID', // required
  mode: 'live', // 'live', 'test', or 'sandbox' (Stripe only)
  provider: 'stripe', // set to 'stripe', 'chargebee', 'braintree', 'paddle'
  record: true, // set to false to skip session playback recording
})
```

## Complete Integration Example

In practice, you'll need to fetch the authHash from your backend before initializing Churnkey. Here's a complete example showing the full flow:

```javascript
// When user clicks cancel button, fetch authHash from your backend
document.getElementById('cancel-button').addEventListener('click', async function () {
  try {
    // Step 1: Fetch the authHash from your backend
    const customerId = 'CUSTOMER_ID'; // Get from your logged-in user session

    const response = await fetch('/api/churnkey', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        customerId: customerId,
      }),
    });

    if (!response.ok) {
      console.error('Failed to fetch authHash');
      return;
    }

    const { userHash } = await response.json();

    // Step 2: Initialize and display Churnkey with the authHash
    window.churnkey.init('show', {
      customerId: customerId, // required unless Paddle
      authHash: userHash, // required - fetched from backend
      subscriptionId: 'SUBSCRIPTION_ID', // recommended unless Paddle
      appId: 'YOUR_APP_ID', // required
      mode: 'live', // 'live', 'test', or 'sandbox' (Stripe only)
      provider: 'stripe', // set to 'stripe', 'chargebee', 'braintree', 'paddle'
      record: true, // set to false to skip session playback recording
    });
  } catch (error) {
    console.error('Error initializing Churnkey:', error);
  }
});
```

### Framework-Specific Examples

::code-group
```javascript [React]
import { useState } from 'react';

function CancelButton() {
  const [loading, setLoading] = useState(false);

  const handleCancel = async () => {
    setLoading(true);
    try {
      const customerId = user.customerId; // From your auth context

      const response = await fetch('/api/churnkey', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ customerId }),
      });

      const { userHash } = await response.json();

      window.churnkey.init('show', {
        customerId: customerId,
        authHash: userHash,
        subscriptionId: user.subscriptionId,
        appId: 'YOUR_APP_ID',
        mode: 'live',
        provider: 'stripe',
        record: true,
      });
    } catch (error) {
      console.error('Error:', error);
    } finally {
      setLoading(false);
    }
  };

  return (
    <button onClick={handleCancel} disabled={loading}>
      {loading ? 'Loading...' : 'Cancel Subscription'}
    </button>
  );
}
```

```vue [Vue.js]
<template>
  <button @click="handleCancel" :disabled="loading">
    {{ loading ? 'Loading...' : 'Cancel Subscription' }}
  </button>
</template>

<script>
export default {
  data() {
    return {
      loading: false,
    };
  },
  methods: {
    async handleCancel() {
      this.loading = true;
      try {
        const customerId = this.$auth.user.customerId; // From your auth

        const response = await fetch('/api/churnkey', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ customerId }),
        });

        const { userHash } = await response.json();

        window.churnkey.init('show', {
          customerId: customerId,
          authHash: userHash,
          subscriptionId: this.$auth.user.subscriptionId,
          appId: 'YOUR_APP_ID',
          mode: 'live',
          provider: 'stripe',
          record: true,
        });
      } catch (error) {
        console.error('Error:', error);
      } finally {
        this.loading = false;
      }
    },
  },
};
</script>
```

```typescript [Next.js]
'use client';

import { useState } from 'react';

export default function CancelButton({ user }: { user: User }) {
  const [loading, setLoading] = useState(false);

  const handleCancel = async () => {
    setLoading(true);
    try {
      const response = await fetch('/api/churnkey', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ customerId: user.customerId }),
      });

      const { userHash } = await response.json();

      window.churnkey.init('show', {
        customerId: user.customerId,
        authHash: userHash,
        subscriptionId: user.subscriptionId,
        appId: process.env.NEXT_PUBLIC_CHURNKEY_APP_ID,
        mode: 'live',
        provider: 'stripe',
        record: true,
      });
    } catch (error) {
      console.error('Error:', error);
    } finally {
      setLoading(false);
    }
  };

  return (
    <button onClick={handleCancel} disabled={loading}>
      {loading ? 'Loading...' : 'Cancel Subscription'}
    </button>
  );
}
```
::

## Troubleshooting Common Issues

### HMAC Authentication Error: "Invalid authHash"

If you receive an authentication error when initializing Churnkey, this indicates that the HMAC hash generated by your backend doesn't match what Churnkey expects.

**Verify your HMAC implementation**

Churnkey provides an HMAC verification tool in the dashboard to help you debug authentication issues:

1. Navigate to **Settings** (bottom left corner)
2. Click **Installation** in the top menu
3. Scroll down to the **Verify Your Server Implementation** section
4. Select the **Production API Key** or **Test API Key** (depending on which environment you're testing)
5. Enter the customerId in the input field
6. Compare this hash with what your backend is producing

<p align="center">
  <img src="./img/cancel_flow/hmac_generator.png" alt="HMAC Hash Generator">
</p>

**Common causes of HMAC mismatches:**

- **Extra whitespace**: Make sure there are no leading or trailing spaces in your Customer ID or API Key when generating the hash.
- **Encoding issues**: The HMAC hash should be a hexadecimal string. Verify that your implementation uses `.digest("hex")` or equivalent in your language.
- **API key mismatch**: Ensure you're using the correct API key for your environment (Test API Key for test mode, Production API Key for live mode).


### Cancel Flow Modal Not Displaying

If the Churnkey modal doesn't appear after clicking the cancel button, this is typically caused by a **timing issue** in server-side rendered (SSR) applications. The Churnkey script attempts to initialize before the page has fully hydrated, preventing the modal from rendering correctly.

**What's happening:**

When your application uses server-side rendering (Next.js, Nuxt, SvelteKit, etc.), the page goes through a hydration process where the server-rendered HTML is converted into an interactive client-side application. If the Churnkey script loads during this hydration phase, `window.churnkey` may not be fully initialized when you call `window.churnkey.init('show')`. While the API call succeeds, the modal interface fails to render.

**Solution:**

Ensure the Churnkey script loads **after** the page has completed hydration. In most modern frameworks, this means loading the script within a client-side lifecycle method or hook.

**Framework-specific implementations:**

::code-group
```javascript [React]
import { useEffect } from 'react';

function App() {
  useEffect(() => {
    // Load Churnkey script after component mounts (hydration complete)
    if (!window.churnkey || !window.churnkey.created) {
      window.churnkey = { created: true };
      const script = document.createElement('script');
      script.src = 'https://assets.churnkey.co/js/app.js?appId=YOUR_APP_ID';
      script.async = true;
      document.head.appendChild(script);
    }
  }, []);

  return <div>{/* Your app content */}</div>;
}
```

```typescript [Next.js]
'use client';

import { useEffect } from 'react';

export default function ChurnkeyProvider({ children }: { children: React.ReactNode }) {
  useEffect(() => {
    // Load Churnkey script after client-side hydration
    if (!window.churnkey || !window.churnkey.created) {
      window.churnkey = { created: true };
      const script = document.createElement('script');
      script.src = `https://assets.churnkey.co/js/app.js?appId=${process.env.NEXT_PUBLIC_CHURNKEY_APP_ID}`;
      script.async = true;
      document.head.appendChild(script);
    }
  }, []);

  return <>{children}</>;
}
```

```vue [Vue.js]
<template>
  <div>
    <!-- Your app content -->
  </div>
</template>

<script>
export default {
  mounted() {
    // Load Churnkey script after component mounts
    if (!window.churnkey || !window.churnkey.created) {
      window.churnkey = { created: true };
      const script = document.createElement('script');
      script.src = 'https://assets.churnkey.co/js/app.js?appId=YOUR_APP_ID';
      script.async = true;
      document.head.appendChild(script);
    }
  },
};
</script>
```
::

## Frequently Asked Questions

### Why do I need to fetch the authHash every time?

The authHash must be fetched fresh each time a customer initiates cancellation. Never hardcode the authHash or cache it on the frontend, as this would compromise security. Fetching it from your backend ensures that:

- Each cancellation request is authenticated through your backend
- Only logged-in customers can generate valid authHash values
- The authHash is tied to the specific customer making the request
