A Consent Management Platform (CMP) is the component that captures a user’s consent choices and translates them into a machine-readable signal that every downstream tag, SDK, and pixel must obey. Choose the wrong one — or integrate the right one incorrectly — and you inherit two failures at once: a compliance gap (vendors firing before a lawful basis exists) and a performance regression (a heavyweight consent bundle blocking the main thread before the user has seen anything). This guide covers how to evaluate a CMP against the two standards that actually matter in production — IAB TCF v2.2 and Google Consent Mode v2 — and how to integrate whichever you pick as the one authoritative consent source that gates all injection.
The decision is architectural, not cosmetic. A CMP is not “the cookie banner”; it is the resolution point for a state machine that fans out to Google Tag Manager, advertising SDKs, session-replay tools, and your own first-party analytics. Once selected, its output becomes the input to your consent gating architecture, your multi-vendor sync layer, and your regional routing rules. Getting the selection criteria right up front prevents a rip-and-replace migration later.
This section splits into four companion guides. Start with the CMP selection criteria for Google Consent Mode v2 to understand the exact signal requirements, then follow the vendor-specific integration references for implementing consent sync with OneTrust and implementing consent sync with Cookiebot. If you need full control of the consent lifecycle, the guide to building a custom CMP consent bus shows how to construct the broker yourself.
Prerequisites and Decision Criteria
Before evaluating vendors, pin down the requirements that determine which CMP can even qualify:
- Regulatory scope. EU/EEA + UK traffic implies IAB TCF v2.2 for programmatic advertising. California and other US states imply the CCPA/CPRA opt-out model and Global Privacy Control (GPC). Multi-jurisdiction sites need a CMP that switches frameworks by geography.
- Ad monetization stack. If you sell programmatic inventory through IAB-registered vendors, the CMP must be TCF v2.2-certified and pass a valid TC string. If you only run Google Ads and GA4, you need Google Consent Mode v2 far more than full TCF.
- Self-hosting and control. Some teams require the consent bundle served from their own origin (for CSP strictness or latency), and some require the consent record stored in their own database rather than a vendor cloud.
- Performance budget. The CMP script sits on the critical path. Its transfer size and main-thread cost directly move LCP and INP. A 90 KB consent bundle that blocks for 120 ms is a measurable regression.
CMP capability comparison
The three realistic options are OneTrust (enterprise SaaS), Cookiebot (mid-market SaaS by Usercentrics), and a purpose-built custom CMP. They trade off on the axes that matter:
| Criterion | OneTrust | Cookiebot | Custom CMP |
|---|---|---|---|
| IAB TCF v2.2 certified | Yes (registered CMP ID) | Yes (registered CMP ID) | Only if you register and pass IAB validation |
| Google Consent Mode v2 | Native, template-driven | Native, auto-mapped | Manual — you emit gtag('consent', …) yourself |
| Geo / regional rules | Extensive (geolocation rulesets) | Built-in (region auto-detect) | You build geo detection at the edge |
| Self-host the bundle | Limited (CDN-first) | No (Cookiebot CDN) | Full — served from your origin |
| Data residency / storage | Vendor cloud (region-selectable) | Vendor cloud (EU) | Your database |
| Cost model | High, seat + volume | Moderate, domain/tier | Engineering time + IAB fees |
| Time to production | Days (config) | Hours (script tag) | Weeks (build + certify) |
Control over __tcfapi timing |
Vendor-controlled | Vendor-controlled | Total |
The pattern most teams land on: OneTrust when compliance/legal owns the tool and needs audit trails and geo rulesets; Cookiebot when engineering wants the fastest correct TCF + Consent Mode v2 integration with minimal config; a custom CMP only when you need the consent bundle self-hosted, the record in your own store, or the __tcfapi/__gpp timing fully under your control. The custom consent bus guide covers the last case.
Concept: What the CMP Actually Emits
A CMP is defined by the interfaces it exposes, not by its banner UI. Three contracts matter.
The IAB TCF v2.2 __tcfapi interface
Under the IAB Transparency & Consent Framework v2.2, a certified CMP registers a global __tcfapi function and encodes user choices into a TC string — a base64url-encoded record of consent and legitimate-interest signals per purpose (1–11) and per vendor from the Global Vendor List. Downstream code reads it through a stable command interface:
// Read the resolved TCF v2.2 state. The addEventListener command fires
// the callback on every state change; only act on resolved statuses.
window.__tcfapi('addEventListener', 2, (tcData, success) => {
if (!success) return;
if (tcData.eventStatus !== 'tcloaded' && tcData.eventStatus !== 'useractioncomplete') return;
// Purpose 1 = "Store and/or access information on a device"
const canStore = tcData.purpose.consents[1] === true;
const tcString = tcData.tcString; // pass to IAB vendors verbatim
});
TCF v2.2 (mandatory since the deprecation of v2.0 in 2023) tightened the rules: purpose 1 can no longer be claimed on a legitimate-interest basis, vendor counts must be disclosed, and the retention period per purpose must be surfaced. A CMP that is not on the current v2.2 certified list will pass strings that IAB vendors reject.
The IAB GPP __gpp interface
For US state laws, the newer Global Privacy Platform exposes a __gpp function carrying section strings (e.g. the US National or per-state sections) instead of a TCF TC string. A multi-region CMP surfaces both __tcfapi (for EU/EEA) and __gpp (for US), and your routing layer reads whichever applies to the resolved region.
Google Consent Mode v2 signals
Independently of IAB, Google’s tags (GA4, Google Ads, Floodlight) obey Consent Mode v2, a set of four boolean-ish signals set through gtag('consent', …):
ad_storage— cookies/identifiers for advertising.analytics_storage— cookies/identifiers for analytics.ad_user_data— whether user data may be sent to Google for ads (new in v2).ad_personalization— whether data may be used for personalized ads / remarketing (new in v2).
The CMP’s job is to set a fully denied default before any Google tag loads, then call update when the user chooses. Consent Mode v2 became mandatory in March 2024 for anyone using Google’s advertising features with EEA traffic; without the two new ad_user_data/ad_personalization signals, remarketing audiences stop populating. The exact mapping is the subject of the Consent Mode v2 selection-criteria guide.
Implementation: The CMP as Single Source of Truth
Whatever vendor you pick, the integration shape is the same: the CMP resolves consent once, and every injection path reads that one resolved state. Do not let individual tags subscribe to the CMP independently — that reintroduces the drift the source-of-truth pattern exists to prevent.
Step 1 — Set a denied default before any tag loads
Consent Mode v2 assumes granted unless told otherwise. Emit the fully denied default synchronously in <head>, before the CMP bundle and before GTM:
<!-- head, before the CMP script and before GTM -->
<script>
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
// Deny everything up front, including the two v2 signals.
gtag('consent', 'default', {
ad_storage: 'denied',
analytics_storage: 'denied',
ad_user_data: 'denied',
ad_personalization: 'denied',
functionality_storage: 'granted', // strictly necessary
security_storage: 'granted',
wait_for_update: 500 // ms grace for the CMP to call update()
});
</script>
Step 2 — Normalize the CMP output into one object
Every CMP exposes consent differently (TCF purposes, OneTrust group IDs, Cookiebot categories). Collapse the vendor-specific shape into a single normalized object that the rest of your code depends on. This is the seam that lets you swap CMPs later:
// consent-source.js — the ONLY module allowed to read the raw CMP.
// Everything else imports getConsent()/onConsentChange() from here.
const listeners = new Set();
let current = {
analytics: false,
ads: false,
adUserData: false,
adPersonalization: false,
tcString: null,
region: 'EU_EEA'
};
export function getConsent() { return { ...current }; }
export function onConsentChange(fn) { listeners.add(fn); return () => listeners.delete(fn); }
function publish(next) {
current = next;
listeners.forEach((fn) => fn(getConsent()));
}
// Subscribe to whichever CMP is present. This adapter is the swap point.
export function bindToCMP() {
if (typeof window.__tcfapi !== 'function') return;
window.__tcfapi('addEventListener', 2, (tcData, ok) => {
if (!ok) return;
if (tcData.eventStatus !== 'tcloaded' && tcData.eventStatus !== 'useractioncomplete') return;
const p = tcData.purpose.consents;
publish({
analytics: p[1] === true && p[8] === true, // store + measure content
ads: p[1] === true && p[3] === true && p[4] === true,
adUserData: p[1] === true,
adPersonalization: p[3] === true && p[4] === true,
tcString: tcData.tcString,
region: document.documentElement.dataset.consentRegion || 'EU_EEA'
});
});
}
Step 3 — Fan out to consumers, never the reverse
Downstream systems subscribe to onConsentChange and translate the normalized state into their own dialect. Google Consent Mode gets a gtag('consent','update',…); IAB vendors get the raw tcString; isolated SDKs get gated. The multi-vendor sync layer is what distributes this fan-out across tabs.
// consumers.js
import { onConsentChange } from './consent-source.js';
onConsentChange((c) => {
// Google Consent Mode v2 — all four signals, every update.
gtag('consent', 'update', {
analytics_storage: c.analytics ? 'granted' : 'denied',
ad_storage: c.ads ? 'granted' : 'denied',
ad_user_data: c.adUserData ? 'granted' : 'denied',
ad_personalization: c.adPersonalization ? 'granted' : 'denied'
});
// Consent-gated injection for non-Google SDKs.
if (c.analytics) injectOnce('session-replay', 'https://cdn.example-replay.com/sdk.js');
});
function injectOnce(id, src) {
if (document.getElementById(id)) return;
const s = document.createElement('script');
s.id = id; s.src = src; s.async = true;
document.head.appendChild(s);
}
Verification Checklist
Interaction with Gating, Sync, and Regional Routing
The CMP selection sits upstream of the rest of this section, and each companion topic consumes its output:
- Architecting GDPR-compliant consent gating. The CMP provides the resolved signal; the gating architecture is the enforcement layer that blocks injection until that signal is affirmative. A CMP without a gate merely records consent while tags fire anyway.
- Syncing consent states across multiple vendors. The normalized object from
consent-source.jsis exactly the payload the sync layer broadcasts across tabs and persists. Choose a CMP whoseaddEventListenerfires reliably on every change, or cross-tab sync degrades. - Regional routing for CCPA and global privacy laws. A multi-region CMP must expose
__gppfor US sections alongside__tcfapifor the EEA. Theregionfield in the normalized object selects which framework’s evaluation applies.
Troubleshooting
Failure mode: __tcfapi is undefined when your adapter runs
Symptom: TypeError: window.__tcfapi is not a function in the console during page load; consumers never receive a consent state.
Cause: Your adapter loads before the CMP stub. TCF-certified CMPs inject a __tcfapi stub early, but a self-hosted or misordered bundle can lose the race.
Fix: Load the CMP (or its stub) first in <head>, then bind. If ordering cannot be guaranteed, poll for typeof window.__tcfapi === 'function' on a 100 ms interval with a timeout, then call bindToCMP().
Failure mode: Google remarketing audiences stop growing after March 2024
Symptom: Google Ads reports audiences no longer collecting; GA4 modeling flags missing consent signals.
Cause: The CMP emits ad_storage/analytics_storage but not the v2 signals ad_user_data/ad_personalization.
Fix: Upgrade the CMP template to a Consent Mode v2 build and confirm all four signals in the update call. See the Consent Mode v2 selection-criteria guide.
Failure mode: IAB vendors reject the TC string
Symptom: Programmatic partners log “invalid or expired TC string” and drop bid requests.
Cause: The CMP is not on the current TCF v2.2 certified list, or is passing a v2.0-shaped string.
Fix: Verify the CMP’s registered ID against the IAB Global Vendor List and confirm it advertises v2.2 support. Non-certified custom CMPs must register and pass IAB validation before their strings are honored.
Frequently Asked Questions
Do I need IAB TCF if I only use Google Analytics and Google Ads?
No. TCF v2.2 governs the IAB programmatic ecosystem. If your only measurement and advertising vendors are Google’s, you need Google Consent Mode v2 (the four gtag('consent', …) signals), not a TC string. Adding full TCF for a Google-only stack is overhead you will maintain for no benefit. Reassess if you onboard IAB-registered demand partners.
Can I switch from OneTrust to Cookiebot later without rewriting everything?
Yes, if you followed the source-of-truth pattern. Only consent-source.js reads the raw CMP; every consumer depends on the normalized object. Swapping CMPs means rewriting one adapter function (bindToCMP) and its purpose/category mapping. If tags read the CMP directly, a migration touches every tag instead.
Should the CMP bundle be self-hosted for performance?
Self-hosting removes a third-party DNS/TCP/TLS round trip and lets you apply a strict CSP without allow-listing the vendor CDN, which can shave 50–150 ms off time-to-resolved-consent. The trade-off is that you lose automatic vendor updates and must ship new bundle versions yourself. OneTrust and Cookiebot are CDN-first; full self-hosting is a reason to consider a custom consent bus.
What is the difference between Consent Mode basic and advanced?
In basic mode you block Google tags entirely until consent, so no pings fire pre-consent. In advanced mode the tags load and send cookieless, anonymized pings before consent, which Google uses for conversion modeling. Advanced mode improves modeled data but means network activity before the user answers — a trade-off your compliance team must sign off. Both require all four v2 signals in the default and update calls.