Skip to content

Klaviyo

Grow your lists with Klaviyo signup forms and keep visitor tracking under consent. By default, Klaviyo.js waits for marketing and measurement.

Inth keeps the consent record of each choice, with the visitor’s country and the policy that applied.

Start for free
pnpm add @c15t/integrations
KlaviyoWaiting for consent
Before a choice

Allow marketing only. Nothing loads from Klaviyo, and the Application tab shows no __kla_id cookie.

Turn the switch on, then off, to see what loads.

Before you start

You need an Inth project that connects to your app and c15t in that app. You also need the six character public API key from Settings > Account > API keys in Klaviyo.

Never put a private key (pk_...) in browser code. If you pass one, the helper logs an error, and Klaviyo does not load.

Open the quickstart for your framework

How consent works with Klaviyo

One script, Klaviyo.js, serves signup forms and Active on Site tracking. Klaviyo has no setting that loads one without the other.

So by default, the c15t klaviyo helper waits for marketing and measurement. With one category or none, c15t requests nothing from Klaviyo, and Klaviyo stores nothing.

Once the second category has permission, Klaviyo.js loads once without a page reload. On withdrawal, c15t removes the loader and reloads the page.

The helper sets the vendor slug klaviyo. So a visitor can allow both categories and still switch Klaviyo off.

Installation

The helper has two modes. Pick forms with tracking, or forms alone.

Installation

Forms only is not a cookieless mode. Klaviyo.js still writes an anonymous __kla_id cookie, and form views still reach Klaviyo form analytics.

Setup steps

  1. Install the integration package

    Add the helpers next to c15t. An import alone does not start Klaviyo.js.

    pnpm add @c15t/integrations
  2. Add your public API key

    The public API key, also called the site ID, appears on every page that loads Klaviyo. So it is safe in browser code.

    src/consent-scripts.ts
    import { klaviyo } from '@c15t/integrations/klaviyo';
    
    export const scripts = [klaviyo({ publicApiKey: 'YOUR_PUBLIC_API_KEY' })];

    Pass your own category condition if your legal review classes Klaviyo in another way.

  3. Send your own events only with consent

    The helper never calls identify or track. The klaviyo object exists only after consent on this page, so optional chaining drops calls made without it.

    src/add-to-cart.ts
    window.klaviyo?.track('Added to Cart', {
      ProductName: product.name,
      Price: product.price,
    });

    This check needs the default reload on revocation. With reloadOnConsentRevoked: false, check the categories before each call.

  4. Register it with your existing setup

    Add scripts to the c15t configuration that you have now. Keep your Inth endpoint, mode, policy, styles and consent UI.

    Add scripts to c15t.config.ts, next to next.config.ts. Keep the rest of your config, such as mode, in the same call. App Router, Pages Router and static export read the same file.

    c15t.config.ts
    import { defineConsentConfig } from 'c15t/next';
    import { scripts } from './src/consent-scripts';
    
    export default defineConsentConfig({ scripts });

    Pass scripts to the ConsentRoot in your root route. Import helpers in the route module, not in a server function: script configurations carry callbacks.

    src/routes/__root.tsx
    import { scripts } from '../consent-scripts';
    
    <ConsentRoot state={consent} scripts={scripts}>

    Add scripts to the options of your ConsentProvider, next to mode. Keep the banner, dialog and preferences link inside the provider.

    src/consent.tsx
    import { scripts } from './consent-scripts';
    
    <ConsentProvider options={{ mode, scripts }}>

    Register scripts under the c15t key in app.config.ts. Keep them out of nuxt.config.ts, which reaches the browser as JSON and drops the helper callbacks.

    app/app.config.ts
    import { scripts } from '../src/consent-scripts';
    
    export default defineAppConfig({
      c15t: { scripts },
    });

    Pass scripts to the c15tVue plugin call you have now. The plugin starts one script loader when the app mounts.

    src/main.ts
    import { c15tVue, manifest } from 'c15t/vue/vue-plugin';
    import { scripts } from './consent-scripts';
    
    app.use(c15tVue, {
      mode: manifest(),
      scripts,
    });

    Add scripts to the client entrypoint, which the integration finds on its own. Keep helpers out of astro.config.mjs: its options are serialized into the page.

    src/c15t.client.ts
    import type { C15tClientOptionsExtension } from 'c15t/astro';
    import { scripts } from './consent-scripts';
    
    export default { scripts } satisfies C15tClientOptionsExtension;

    Pass scripts as a prop to the ConsentProvider you have now. The provider owns the loader and disposes it on unmount.

    src/App.svelte
    <script lang="ts">
      import { ConsentProvider, manifest } from 'c15t/svelte';
      import { scripts } from './consent-scripts';
    </script>
    
    <ConsentProvider mode={manifest()} {scripts}>
      <!-- Your app and consent UI -->
    </ConsentProvider>

    Add scripts to the ConsentRoot in the root layout. Import helpers in the layout component, not in +layout.server.ts: a server load cannot send functions to the browser.

    src/routes/+layout.svelte
    <script lang="ts">
      import { ConsentRoot } from 'c15t/svelte';
      import { scripts } from '../consent-scripts';
    
      let { children, data } = $props();
    </script>
    
    <ConsentRoot state={data.consent} {scripts}>
      {@render children()}
    </ConsentRoot>

    Paste the vendor’s snippet behind consent

    The helpers need a bundler. On a page with the c15t script tag, paste the vendor’s own snippet instead and keep it inert until visitors allow its category. Helper options, such as loadMode, do not apply.

    index.html
    <script type="text/plain" data-c15t-category="marketing">
      // The vendor's snippet, unchanged
    </script>

    Pass scripts to init() from @c15t/browser, next to your mode. With createConsentRuntime, pass scripts to it instead.

    src/consent.ts
    import { init, manifest } from '@c15t/browser';
    import { scripts } from './consent-scripts';
    
    const consent = init({
      mode: manifest(),
      scripts,
    });
All frameworks

Remove the Klaviyo snippet first. On Shopify, BigCommerce, WooCommerce and other platforms where Klaviyo adds Klaviyo.js for you, turn that off.

Verify your setup

Filter DevTools Network by klaviyo. Then test the three states.

  • Before a choice

    Allow marketing only. Nothing loads from Klaviyo, and the Application tab shows no __kla_id cookie.

  • After you allow both

    Allow measurement too. klaviyo.js loads from static.klaviyo.com/onsite/js/ with your key, followed by its modules.

  • After you withdraw consent

    c15t removes the loader and reloads the page. The new page loads nothing from Klaviyo.

Cookie consent is not subscription consent. Your Klaviyo forms still need their own email and SMS consent, in the words that your channels need.

Open the full Klaviyo guide