Introduction
CookiePilot is a consent management platform (CMP), compliant with the GDPR, the ePrivacy rules (the PECR in the UK) and Google Consent Mode v2.
Who it's for
- Site owners: deploy with no coding required.
- Developers: API, events, GTM integrations.
- Agencies: white-label, multi-domain management.
Quick start
Step 1: Sign up
- Create an account at app.cookiepilot.io/register.
- Add your domain in the dashboard and copy the API key (format
cp_live_...).
Step 2: Install the code
A direct install is two scripts in the <head>, in this order.
Step 2a: Default consent (inline stub). Paste this first, before any other script (cookiepilot.js, GA, GTM, advertising and tracking tags):
<script>"use strict";(function(){window.dataLayer=window.dataLayer||[];var d={ad_storage:"denied",ad_user_data:"denied",ad_personalization:"denied",analytics_storage:"denied",functionality_storage:"denied",personalization_storage:"denied",security_storage:"granted",wait_for_update:500},h=false;try{for(var i=0;i<window.dataLayer.length;i++){var x=window.dataLayer[i];if(x&&x[0]==="consent"&&x[1]==="default"){h=true;break}}}catch(err){}if(!h&&window.dataLayer.length)window.dataLayer.unshift(["consent","default",d]);window.gtag=function(){window.dataLayer.push(arguments)};if(!h)window.gtag("consent","default",d);var a=document.cookie.match(/(^|)cookiepilot_consent=([^;]+)/);if(a){try{var e=JSON.parse(decodeURIComponent(a[2]));window.gtag("consent","update",{analytics_storage:e.analytics?"granted":"denied",ad_storage:e.marketing?"granted":"denied",ad_user_data:e.marketing?"granted":"denied",ad_personalization:e.marketing?"granted":"denied",functionality_storage:e.preferences?"granted":"denied",personalization_storage:e.preferences?"granted":"denied",security_storage:"granted"})}catch(err){}}})();</script>
This inline snippet immediately sets every category to denied (with wait_for_update: 500), so the default Google Consent Mode v2 state is ready before anything else loads. For a returning visitor it reads the saved consent straight from the cookie and fires a consent update.
Step 2b: Banner script. Add this right after the inline stub:
<!-- CookiePilot -->
<script async src="https://cdn.cookiepilot.io/cookiepilot.js" data-cpkey="TWOJ_KLUCZ"></script>
Step 3: Configure the banner
In the dashboard: Domain → Configuration → Appearance:
- banner position (top, bottom, modal),
- colours and copy,
- a floating "Cookie settings" button for returning visitors.
Google Tag Manager integration
Through GTM you install CookiePilot with a single Custom HTML tag that sets the default consent state and loads the banner at the earliest GTM phase. You don't need a separate stub file or a second tag.
Tag: CookiePilot - Consent Init + Banner
In GTM, create a Custom HTML tag (Tags → New → Custom HTML) and paste:
<script>"use strict";
(function() {
window.dataLayer=window.dataLayer||[];var d={ad_storage:"denied",ad_user_data:"denied",ad_personalization:"denied",analytics_storage:"denied",functionality_storage:"denied",personalization_storage:"denied",security_storage:"granted",wait_for_update:500},h=false;try{for(var i=0;i<window.dataLayer.length;i++){var x=window.dataLayer[i];if(x&&x[0]==="consent"&&x[1]==="default"){h=true;break}}}catch(err){}if(!h&&window.dataLayer.length)window.dataLayer.unshift(["consent","default",d]);window.gtag=function(){window.dataLayer.push(arguments)};if(!h)window.gtag("consent","default",d);var a=document.cookie.match(/(^|)cookiepilot_consent=([^;]+)/);if(a){try{var e=JSON.parse(decodeURIComponent(a[2]));window.gtag("consent","update",{analytics_storage:e.analytics?"granted":"denied",ad_storage:e.marketing?"granted":"denied",ad_user_data:e.marketing?"granted":"denied",ad_personalization:e.marketing?"granted":"denied",functionality_storage:e.preferences?"granted":"denied",personalization_storage:e.preferences?"granted":"denied",security_storage:"granted"})}catch(err){}}
var s = document.createElement('script');
s.src = 'https://cdn.cookiepilot.io/cookiepilot.js?cpkey=' + encodeURIComponent('TWOJ_KLUCZ');
document.head.appendChild(s);
})();
</script>
Trigger: Consent Initialization - All Pages. The tag must fire once per page. Google tags (GA4, Google Ads), Facebook Pixel and other marketing tags do NOT use the Consent Initialization trigger; they fire later (Consent Checks or a consent-dependent trigger). Save the tag and publish the GTM container.
Order and triggers
| Order | Tag | Trigger |
|---|---|---|
| 1 | CookiePilot - Consent Init + Banner | Consent Initialization - All Pages |
| 2 | GA4, Google Ads, UET | All Pages (Consent Mode handles consent itself) |
| 3 | Facebook Pixel, TikTok, LinkedIn, etc. | Custom Event cookiepilot_consent_update + consent condition (see below) |
Non-Google tags (Facebook Pixel, TikTok, LinkedIn)
Google Consent Mode only covers Google tags. For every other script, the widget pushes an event to the dataLayer on each consent change:
dataLayer.push({
event: 'cookiepilot_consent_update',
cookiepilot_consent: {
necessary: true,
analytics: true,
marketing: true,
preferences: false
}
});
The event also fires on every visit from a returning user (when the widget reads the consent cookie), so the GTM trigger works on every visit, not just on the first decision.
Shared pattern (do this once)
You configure these three pieces once, then reuse them for all non-Google tags.
- Trigger (Triggers → New → Custom Event): Event name
cookiepilot_consent_update. No conditions, no "Once per page" (the tag must be able to fire again after a changed decision). - Data Layer Variable for each category you use:
- Name
cookiepilot_consent.marketing→ variable e.g.dlv.cp_marketing - Name
cookiepilot_consent.analytics→ variable e.g.dlv.cp_analytics - Name
cookiepilot_consent.preferences→ variable e.g.dlv.cp_preferences
- Name
- Trigger Group or a condition on the trigger:
dlv.cp_marketing equals true(for marketing tags) or the relevant field.
In each of the examples below, the Trigger is the same Custom Event with a condition added on the right variable.
Facebook Pixel
Tag in GTM: Tags → New → Custom HTML.
<script>
!function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?
n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;
n.push=n;n.loaded=!0;n.version='2.0';n.queue=[];t=b.createElement(e);t.async=!0;
t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window,
document,'script','https://connect.facebook.net/en_US/fbevents.js');
fbq('init', 'YOUR_PIXEL_ID');
fbq('track', 'PageView');
</script>
- Trigger:
cookiepilot_consent_update+ conditiondlv.cp_marketing equals true. - Tag firing options: Once per page.
Optionally, for full compliance with Facebook Limited Data Use, add a second tag that calls fbq('consent','revoke') with the trigger dlv.cp_marketing equals false.
TikTok Pixel
Tag in GTM: Tags → New → Custom HTML.
<script>
!function (w, d, t) {
w.TiktokAnalyticsObject=t;var ttq=w[t]=w[t]||[];ttq.methods=["page","track","identify","instances","debug","on","off","once","ready","alias","group","enableCookie","disableCookie","holdConsent","revokeConsent","grantConsent"],ttq.setAndDefer=function(t,e){t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}};for(var i=0;i<ttq.methods.length;i++)ttq.setAndDefer(ttq,ttq.methods[i]);ttq.instance=function(t){for(var e=ttq._i[t]||[],n=0;n<ttq.methods.length;n++)ttq.setAndDefer(e,ttq.methods[n]);return e},ttq.load=function(e,n){var r="https://analytics.tiktok.com/i18n/pixel/events.js",o=n&&n.partner;ttq._i=ttq._i||{},ttq._i[e]=[],ttq._i[e]._u=r,ttq._t=ttq._t||{},ttq._t[e]=+new Date,ttq._o=ttq._o||{},ttq._o[e]=n||{};n=document.createElement("script");n.type="text/javascript",n.async=!0,n.src=r+"?sdkid="+e+"&lib="+t;e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(n,e)};
ttq.load('YOUR_TIKTOK_PIXEL_ID');
ttq.grantConsent();
ttq.page();
}(window, document, 'ttq');
</script>
- Trigger:
cookiepilot_consent_update+ conditiondlv.cp_marketing equals true. - Tag firing options: Once per page.
ttq.grantConsent() is TikTok's new API (introduced in 2024). Without it, TikTok receives hashed data without consent, which breaks the terms.
LinkedIn Insight Tag
<script type="text/javascript">
_linkedin_partner_id = "YOUR_LINKEDIN_PARTNER_ID";
window._linkedin_data_partner_ids = window._linkedin_data_partner_ids || [];
window._linkedin_data_partner_ids.push(_linkedin_partner_id);
</script>
<script type="text/javascript">
(function(l) {
if (!l){window.lintrk = function(a,b){window.lintrk.q.push([a,b])};
window.lintrk.q=[]}
var s = document.getElementsByTagName("script")[0];
var b = document.createElement("script");
b.type = "text/javascript";b.async = true;
b.src = "https://snap.licdn.com/li.lms-analytics/insight.min.js";
s.parentNode.insertBefore(b, s);})(window.lintrk);
</script>
- Trigger:
cookiepilot_consent_update+ conditiondlv.cp_marketing equals true. - Tag firing options: Once per page.
Hotjar
<script>
(function(h,o,t,j,a,r){
h.hj=h.hj||function(){(h.hj.q=h.hj.q||[]).push(arguments)};
h._hjSettings={hjid:YOUR_HOTJAR_ID,hjsv:6};
a=o.getElementsByTagName('head')[0];
r=o.createElement('script');r.async=1;
r.src=t+h._hjSettings.hjid+j+h._hjSettings.hjsv;
a.appendChild(r);
})(window,document,'https://static.hotjar.com/c/hotjar-',".js?sv=");
</script>
- Trigger:
cookiepilot_consent_update+ conditiondlv.cp_analytics equals true. - Tag firing options: Once per page.
Hotjar goes under analytics, not marketing (it measures behaviour, not ads). Check your own cookie policy — some companies classify Hotjar differently.
Microsoft Clarity
<script type="text/javascript">
(function(c,l,a,r,i,t,y){
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
})(window, document, "clarity", "script", "YOUR_CLARITY_PROJECT_ID");
</script>
- Trigger:
cookiepilot_consent_update+ conditiondlv.cp_analytics equals true. - Tag firing options: Once per page.
Pinterest Tag
<script>
!function(e){if(!window.pintrk){window.pintrk = function () {
window.pintrk.queue.push(Array.prototype.slice.call(arguments))};var
n=window.pintrk;n.queue=[],n.version="3.0";var
t=document.createElement("script");t.async=!0,t.src=e;var
r=document.getElementsByTagName("script")[0];
r.parentNode.insertBefore(t,r)}}("https://s.pinimg.com/ct/core.js");
pintrk('load', 'YOUR_PINTEREST_TAG_ID');
pintrk('page');
</script>
- Trigger:
cookiepilot_consent_update+ conditiondlv.cp_marketing equals true. - Tag firing options: Once per page.
Microsoft Ads (UET)
UET has supported Google Consent Mode since late 2023, so you don't need a Custom Event trigger. Add the tag the standard way (All Pages, Once per page) and UET reads ad_storage from GCM itself, which the widget sets.
Category mapping (summary)
| Tag | Category | Field in cookiepilot_consent |
|---|---|---|
| Facebook Pixel | marketing | marketing |
| TikTok Pixel | marketing | marketing |
| LinkedIn Insight | marketing | marketing |
| marketing | marketing | |
| Hotjar | analytics | analytics |
| Microsoft Clarity | analytics | analytics |
| Mixpanel, Amplitude | analytics | analytics |
| Intercom, Drift, Crisp | preferences | preferences |
| GA4, Google Ads, UET | (GCM, no trigger) | handled by gtag('consent','update') |
API Reference
CookiePilot exposes the window.CookiePilot object:
| Method | Description |
|---|---|
CookiePilot.getConsent() | The current consent state, or null if no decision has been made. |
CookiePilot.acceptAll() | Consent to all categories. |
CookiePilot.rejectAll() | Rejects everything except necessary. |
CookiePilot.updateConsent(partial) | Updates selected categories, e.g. { analytics: true }. |
CookiePilot.showSettings() | Opens the preferences modal. |
CookiePilot.hideSettings() | Closes the preferences modal. |
CookiePilot.showMyConsent() / hideMyConsent() | Shows/hides the floating button. |
Example
const consent = CookiePilot.getConsent();
// { necessary: true, analytics: true, marketing: false, preferences: false }
CookiePilot.updateConsent({ analytics: true });
document.getElementById('cookie-settings').addEventListener('click', () => {
CookiePilot.showSettings();
});
"Manage cookies" link in the footer
<a href="#" onclick="CookiePilot.showSettings(); return false;">Manage cookies</a>
JavaScript events
The widget dispatches a native cookiepilot:consent event on window. The listener must be registered before the widget loads if you want to catch the event for a returning visitor (the dispatch fires immediately once the widget starts):
<script>
window.addEventListener('cookiepilot:consent', (e) => {
if (e.detail.marketing) {
fbq('init', 'YOUR_PIXEL_ID');
}
});
</script>
<script src="https://cdn.cookiepilot.io/cookiepilot.js" data-cpkey="TWOJ_KLUCZ"></script>
e.detail contains the same payload as getConsent(). For GTM-based integrations, use the dataLayer event described above instead of this one, because the dataLayer is a persistent array and GTM picks up historical events.
Consent categories
| Category | Description | Default |
|---|---|---|
necessary | Required for the site to work | Always on |
analytics | Statistics and analytics | Consent required |
marketing | Advertising and remarketing | Consent required |
preferences | Personalisation, language | Consent required |
Mapping to Google Consent Mode
| CookiePilot category | Consent Mode fields |
|---|---|
analytics | analytics_storage |
marketing | ad_storage, ad_user_data, ad_personalization |
preferences | functionality_storage, personalization_storage |
| (always) | security_storage: granted |
Google Consent Mode v2
Version 2 added ad_user_data and ad_personalization (required since March 2024 in the EU/EEA for Google advertising).
| Parameter | Description |
|---|---|
ad_storage | Advertising cookies |
analytics_storage | Analytics cookies |
ad_user_data | Sending user data to Google |
ad_personalization | Ad personalisation |
functionality_storage | Functional cookies |
personalization_storage | Personalisation cookies |
security_storage | Always granted |
How it works:
- The stub sets every field to
deniedsynchronously, withwait_for_update: 500. - After the user's decision, the widget fires
gtag('consent', 'update', {...})using the mapping above. - For a returning visitor, step 2 fires immediately once the widget starts, based on the cookie.
Appearance configuration
In the dashboard: Domain → Configuration:
- Appearance: position, colours, layout (BAR / BOX / MODAL).
- Copy: heading, description, button labels, category descriptions. 13 languages (EN, PL, DE, FR, ES, IT, NL, PT, SV, CS, RO, EL, HU).
- Consent button: a floating "Cookie settings" button shown after the first decision (bottom left/right).
- Custom CSS: a field for your own styles. The widget renders in the Shadow DOM, so CSS selectors from the main document won't work. Use this field only.
Accessibility (WCAG 2.1 AA)
- ✅ Keyboard navigation (Tab, Shift+Tab, Enter, Escape).
- ✅ ARIA labels, role="dialog", aria-modal.
- ✅ Focus trap in the modal.
- ✅ Screen reader support (live regions on state change).
- ✅ Responsive design.
Integrations
WordPress
We have an official plugin: CookiePilot on WordPress.org.
- WordPress admin → Plugins → Add New → search for "CookiePilot".
- Install and activate.
- Settings → CookiePilot → paste the API key from the dashboard.
The plugin inserts the stub and the tag into the <head> itself (in the correct order, ahead of other scripts) and provides a [cookiepilot_settings] shortcode for a "Manage cookies" link in the footer.
If you prefer not to use the plugin, use "Insert Headers and Footers" and paste the snippet from Step 2 of the Quick start into the Header section.
Shopify
- Store → Themes → Edit code.
- In
theme.liquid, paste the snippet before</head>.
Next.js
// app/layout.tsx
import Script from 'next/script';
export default function RootLayout({ children }) {
return (
<html>
<head>
<Script
src="https://cdn.cookiepilot.io/cookiepilot.js"
data-cpkey="TWOJ_KLUCZ"
strategy="beforeInteractive"
/>
</head>
<body>{children}</body>
</html>
);
}
strategy="beforeInteractive" ensures the banner script runs early. Remember that the inline stub from Step 2a (default consent) is added separately, in the <head> ahead of this script — for example via next/script with dangerouslySetInnerHTML or directly in app/layout.tsx.
FAQ
Does the script slow down the site?
The bundle is around 12 KB gzipped and loads asynchronously. No impact on Core Web Vitals.
How long is consent stored?
The user's decision cookie: 365 days by default (configurable in the dashboard). Analytics events: 2 years (ClickHouse TTL).
Does the cookiepilot_consent_update event fire for returning visitors?
Yes. The widget reads the cookie on start and pushes the event to the dataLayer. The Custom Event trigger in GTM fires on every visit, not just on the first decision.
Where do I report a problem?
kontakt@cookiepilot.io or the chat in the dashboard.
Post-deployment testing checklist
Once CookiePilot is installed, verify the configuration before launching advertising campaigns.
1. Test before the user gives consent
- Open the site in incognito mode and clear cookies for the domain.
- Open DevTools → Network and reload the page.
- Before clicking consent, confirm that:
- no analytics/marketing cookies are created, e.g.
_ga,_gcl_*,_fbp,_ttp, - marketing tags don't send requests to Meta/TikTok/LinkedIn before marketing consent,
- the
dataLayerhas the defaultdeniedstate forad_storage,ad_user_data,ad_personalizationandanalytics_storage.
- no analytics/marketing cookies are created, e.g.
2. Test after accepting consent
- Click "Accept all".
- In DevTools, check that the
cookiepilot_consent_updateevent appeared. - For Google tags, check in GTM Preview / Tag Assistant that Consent Mode switched the state to
grantedfor the relevant categories. - For GA4, check DebugView to confirm events start arriving after consent.
- For Meta Pixel / TikTok / LinkedIn, check that the tags fire only after the
cookiepilot_consent_updateevent and the marketing consent condition.
3. Test rejecting consent
- Clear cookies and reload the page.
- Click "Reject all".
- Confirm that marketing cookies and requests are still not triggered.
- Confirm that essential site features still work.
4. Common mistakes
- The GA4/Google Ads tag fires on
Consent Initializationinstead of a later trigger. - The Meta Pixel or TikTok Pixel has no condition on
cookiepilot_consent.marketing. - An old, hardcoded tracker is still in the
<head>before CookiePilot. - The documentation or template still has an outdated script URL or an old identifier attribute instead of the current
cookiepilot.jswithdata-cpkey.