Compliance

Cookie Consent: GDPR and CCPA Compliant Implementation Guide

A practical technical guide to implementing cookie consent banners that satisfy GDPR's IAB TCF 2.2 requirements and CCPA opt-out rules, including CMP configuration, GTM consent mode, and consent withdrawal.

August 1, 20258 min readShipSafer Team

Cookie consent is one of those areas where engineering teams often reach for a third-party widget, click through the setup wizard, and assume the job is done. In practice, a misconfigured consent banner is worse than no banner — it creates the appearance of compliance while leaving real legal exposure. This guide covers what compliant cookie consent actually requires technically, how to configure the major Consent Management Platforms (CMPs), and how to wire everything together so scripts genuinely do not fire before consent is granted.

Why Cookie Consent Is a Technical Problem, Not Just a Legal One

The core requirement under GDPR Article 6 is that processing personal data via cookies must have a lawful basis. For non-essential cookies — analytics, advertising, personalization — the only available lawful basis is explicit, informed, freely given, and specific consent. The CCPA equivalent requires a "Do Not Sell or Share My Personal Information" opt-out for California residents.

What this means technically: scripts that set non-essential cookies must not execute until the user has actively consented. A banner that loads Google Analytics before the user has clicked "Accept" is non-compliant, regardless of how well-worded the banner text is. Fines under GDPR can reach 4% of global annual turnover; the CNIL (France's DPA) has fined Google €150 million and Meta €60 million specifically for making consent rejection harder than acceptance.

The IAB Transparency and Consent Framework 2.2

The IAB TCF 2.2 is an industry-standard protocol for communicating consent signals between CMPs, publishers, and ad tech vendors. Understanding its structure helps you configure your CMP correctly.

The framework operates through a TC String — a base64-encoded binary blob stored in a first-party cookie (euconsent-v2) that encodes:

  • Which of the 10 IAB purposes the user consented to (legitimate interest vs. consent)
  • Which vendors are permitted to process data
  • The CMP ID and version
  • Timestamp of consent

Vendors registered in the IAB Global Vendor List (GVL) declare which purposes they require. Your CMP reads these declarations and presents the appropriate UI. When a user grants or denies consent, the CMP writes the TC String and exposes it via the __tcfapi() JavaScript function.

Key TCF purposes relevant to most SaaS products:

  • Purpose 1: Store and/or access information on a device (required for any non-essential cookie)
  • Purpose 2: Select basic ads
  • Purpose 7: Measure ad performance
  • Purpose 8: Measure content performance (analytics)
  • Purpose 10: Develop and improve products

If you use Google Analytics 4, it maps to Purposes 1, 7, 8, and 10. Your CMP must collect consent for all applicable purposes before firing GA4.

Configuring OneTrust

OneTrust is the most widely deployed enterprise CMP. The critical configuration points:

Script blocking: In the OneTrust dashboard, navigate to Categorization > Script Manager. For each third-party script, assign it to a cookie category (Strictly Necessary, Performance, Functional, Targeting). OneTrust rewrites the script tag's type attribute from text/javascript to text/plain and adds a class attribute matching the category. Scripts with type="text/plain" are not executed by the browser. OneTrust's SDK activates them only after consent.

<!-- Before consent, OneTrust renders this -->
<script type="text/plain" class="optanon-category-C0002">
  // Google Analytics code
</script>

<!-- After consent for category C0002, OneTrust changes type to text/javascript -->

Auto-blocking: OneTrust's auto-blocking feature attempts to intercept dynamically injected scripts. It works by overriding document.createElement and Element.prototype.appendChild. This approach is fragile — scripts loaded via service workers or iframes will bypass it. Always prefer explicit script categorization over relying solely on auto-blocking.

Geolocation rules: Configure separate consent models for EU (opt-in required, TCF 2.2), California (opt-out model, CPRA), and other regions. OneTrust's geolocation engine uses the visitor's IP to serve the appropriate banner variant.

Consent storage: OneTrust stores consent in OptanonConsent (first-party cookie) and OptanonAlertBoxClosed. For multi-domain setups, use OneTrust's cross-domain consent feature to sync consent state via postMessage.

Configuring Cookiebot

Cookiebot takes a different approach — it scans your site via a crawler to detect cookies, then auto-categorizes them. This is convenient but requires review, as the crawler may miss cookies set by JavaScript that runs after interaction.

In Cookiebot's configuration:

  1. Cookie scan: Run a full scan and review the detected cookies. Manually categorize any cookies the auto-scanner missed.
  2. Script blocking: Add data-cookieconsent="statistics" (or marketing, preferences) attributes to script tags. Cookiebot blocks scripts with these attributes until the corresponding category is accepted.
  3. Prior consent: Enable "Require prior consent" to ensure the consent banner appears before any non-essential scripts load. Without this, there is a race condition where scripts may fire before Cookiebot initializes.
<script data-cookieconsent="statistics" async src="https://www.googletagmanager.com/gtm.js?id=GTM-XXXX"></script>

Google Tag Manager Consent Mode v2

GTM Consent Mode v2 (required by Google since March 2024 for EU traffic) introduces a consent state API that Google's tags check before executing. This is separate from your CMP — it is a bridge between your CMP and Google's tag ecosystem.

The two required consent types for basic functionality:

  • ad_storage: Controls whether Google Ads cookies are set
  • analytics_storage: Controls whether GA4 measurement cookies are set

Additional types for full functionality:

  • ad_user_data: Consent to send user data to Google for advertising
  • ad_personalization: Consent to personalized advertising

Set default states before GTM loads:

<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}

  // Set defaults BEFORE GTM script tag
  gtag('consent', 'default', {
    'ad_storage': 'denied',
    'ad_user_data': 'denied',
    'ad_personalization': 'denied',
    'analytics_storage': 'denied',
    'wait_for_update': 500 // ms to wait for CMP signal
  });
</script>
<!-- GTM script tag follows here -->

When your CMP resolves consent, it must call gtag('consent', 'update', {...}) with the appropriate granted/denied values. Most major CMPs have native GTM Consent Mode v2 integrations — verify your CMP version supports v2, not just v1.

With analytics_storage: 'denied', GA4 operates in cookieless mode: it still sends pings but does not set cookies or persist user identifiers. This enables basic measurement without consent, but session attribution and user-level metrics are lost.

Consent Before Script Loading: Implementation Pattern

The safest implementation loads all non-essential scripts conditionally, after consent is confirmed. Here is a pattern using a vanilla JS consent manager:

// consent-manager.js
const CONSENT_COOKIE = 'user_consent';
const CONSENT_CATEGORIES = ['analytics', 'marketing', 'functional'];

function getStoredConsent() {
  const cookie = document.cookie
    .split('; ')
    .find(row => row.startsWith(`${CONSENT_COOKIE}=`));
  if (!cookie) return null;
  try {
    return JSON.parse(decodeURIComponent(cookie.split('=')[1]));
  } catch {
    return null;
  }
}

function setConsent(categories) {
  const value = JSON.stringify({
    categories,
    timestamp: Date.now(),
    version: '1.0'
  });
  const expires = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toUTCString();
  document.cookie = `${CONSENT_COOKIE}=${encodeURIComponent(value)}; expires=${expires}; path=/; SameSite=Lax; Secure`;
  activateConsented(categories);
}

function activateConsented(categories) {
  if (categories.includes('analytics')) {
    loadAnalytics();
  }
  if (categories.includes('marketing')) {
    loadMarketing();
  }
}

function loadAnalytics() {
  const script = document.createElement('script');
  script.src = 'https://www.googletagmanager.com/gtag/js?id=G-XXXX';
  script.async = true;
  document.head.appendChild(script);
}

This pattern ensures scripts are injected into the DOM only after the consent decision is stored. No race conditions, no auto-blocking fragility.

Consent Withdrawal

GDPR Article 7(3) requires that withdrawal of consent be as easy as giving it. Technically, this means:

  1. Accessible withdrawal UI: A persistent link (often in the footer — "Cookie Settings" or "Manage Preferences") must re-open the consent banner and allow category-level changes.
  2. Immediate effect: When a user withdraws consent for analytics, analytics cookies must be deleted and the script must stop executing in the current session.
  3. Cascade to processors: Your CMP should signal to third-party tools (via TCF or direct API calls) that consent has been withdrawn.

For cookie deletion on withdrawal, your CMP should clear cookies matching the withdrawn category. For first-party cookies your app sets directly, you need explicit deletion logic:

function withdrawConsent(category) {
  if (category === 'analytics') {
    // Clear GA4 cookies
    ['_ga', '_gid', '_gat'].forEach(name => {
      document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=.${location.hostname}`;
    });
    // Disable GA4 data collection
    window['ga-disable-G-XXXX'] = true;
  }
}

CCPA Opt-Out Implementation

CCPA/CPRA does not require opt-in consent for most cookies — it requires a "Do Not Sell or Share" opt-out mechanism for California residents. The Global Privacy Control (GPC) signal (a browser-level opt-out) must be honored automatically without requiring the user to interact with a banner.

Check for GPC in your server-side middleware or client-side initialization:

// Check for GPC signal
if (navigator.globalPrivacyControl === true) {
  // Honor opt-out automatically
  setConsent({ marketing: false, analytics: true }); // Analytics may still be permitted
}

For CMPs, enable the GPC auto-detection setting. OneTrust and Cookiebot both support this. The opt-out state should be stored and respected for the duration of the session and ideally persisted via a first-party cookie.

Testing Your Implementation

Use these methods to verify compliance:

  • Browser DevTools: Open the Network tab before consenting. Confirm no requests to analytics or ad domains (google-analytics.com, doubleclick.net, facebook.com/tr) appear before you click "Accept."
  • Cookie audit: In Chrome DevTools > Application > Cookies, confirm no non-essential cookies exist before consent.
  • TCF API check: Run __tcfapi('getTCData', 2, (tcData) => console.log(tcData)) in the console to inspect the current TC String and purpose consents.
  • Automated scanning: Tools like Cookiebot's scanner or CookieMetrix can crawl your site and report pre-consent cookie firing.
  • GPC testing: Install a GPC-enabled browser extension (Privacy Badger supports GPC) and verify your site honors the signal.

A properly implemented consent system is not a checkbox — it is an ongoing technical control that needs to be re-tested whenever you add new scripts, update your CMP, or change your GTM configuration. Treat it like any other security control: audit it regularly.

GDPR
CCPA
cookie consent
privacy
IAB TCF
OneTrust
Cookiebot
GTM

Check Your Security Score — Free

See exactly how your domain scores on DMARC, TLS, HTTP headers, and 25+ other automated security checks in under 60 seconds.