IAB TCF v2.2 implementer quick reference

last verified · against IAB TCF v2.2 CMP API spec and live GVL v3 as of 2026-07

The IAB TCF v2.2 CMP API on one card: __tcfapi commands, PingReturn and TCData fields, eventStatus values, the 11 purposes, and the getTCData deprecation.

What this is

The IAB Transparency and Consent Framework (TCF) v2.2 exposes one in-page function, __tcfapi, through which tags read a user’s consent and legitimate-interest signals from a Consent Management Platform (CMP). This card lists its commands, return objects, enum values, the Global Vendor List purpose set, and the v2.2 behavior changes that break integrations written for older versions.

The interface

__tcfapi signature
__tcfapi(command, version, callback, parameter)

window.__tcfapi must always be a function — including while only the stub is in place during init — never any other type. version is the API major version, which is 2 for all of TCF v2.x. parameter is command-specific and often omitted.

Commands

Command Required? Signature and notes
ping Required __tcfapi('ping', 2, cb) returns a PingReturn; safe to call before the CMP loads
addEventListener Required Registers cb(tcData, success); fires immediately with current data, then on every TC String change; assigns a numeric listenerId
removeEventListener Required __tcfapi('removeEventListener', 2, cb, listenerId); cb(success)
getTCData Optional — DEPRECATED in v2.2 (May 2023) Replaced by an addEventListener callback; many v2.2 CMPs do not implement it
getInAppTCData Optional Mobile in-app; reads TC data from device storage (NSUserDefaults on iOS, SharedPreferences on Android)
getVendorList Optional Returns a GlobalVendorList; accepts an optional version parameter to fetch a specific GVL version

Only getTCData is deprecated. getInAppTCData and getVendorList remain current optional commands.

PingReturn fields

Field Type Meaning
gdprApplies Boolean / undefined Whether GDPR applies in this context
cmpLoaded Boolean true once the real CMP has replaced the stub
cmpStatus String stub, loading (deprecated), loaded, or error
displayStatus String visible, hidden, or disabled
apiVersion String Supported API version string
cmpId Number / undefined IAB-assigned CMP id
cmpVersion Number / undefined CMP version
gvlVersion Number / undefined Global Vendor List version in use
tcfPolicyVersion Number / undefined Policy version; >= 4 indicates TCF v2.2

Status enums

cmpStatus (in PingReturn and TCData):

Value Meaning
stub CMP not yet loaded; the stub is still in place
loading DEPRECATED — not distinct; will be removed in a future version
loaded CMP has finished loading
error CMP is in an error state and shall not respond to any other API request

displayStatus (in PingReturn):

Value Meaning
visible The user interface is currently displayed
hidden The user interface is not yet, or no longer, displayed
disabled The user interface will not show (e.g. GDPR does not apply, or TC data is current)

eventStatus (delivered to addEventListener callbacks):

Value Meaning
tcloaded CMP loaded and prepared to surface a TC String; a valid existing TC String is available; UI not surfaced
cmpuishown UI is surfaced or re-surfaced with transparency rendered per policy
useractioncomplete User has confirmed or re-confirmed choices; CMP is prepared to respond with the corresponding TC String

Gate new consent-dependent processing on useractioncomplete. Use tcloaded only to honor an already-stored valid TC String, and handle cmpuishown by waiting for the user to confirm.

TCData object (when GDPR applies)

Field Notes
tcString The encoded TC String (absent when GDPR does not apply)
tcfPolicyVersion Policy version carried in the string
cmpId, cmpVersion Numbers
gdprApplies Boolean — branch on this first
eventStatus tcloaded, cmpuishown, or useractioncomplete
cmpStatus As in PingReturn
listenerId Present only in addEventListener callbacks
isServiceSpecific Boolean
useNonStandardTexts Boolean
publisherCC Two-letter ISO country code
purposeOneTreatment Boolean
purpose.consents, purpose.legitimateInterests Map: purpose id → Boolean
vendor.consents, vendor.legitimateInterests, vendor.disclosedVendors Map: vendor id → Boolean
specialFeatureOptins Map: special feature id → Boolean
publisher.consents, .legitimateInterests, .customPurpose, .restrictions Publisher overrides; restrictions keyed purpose id, then vendor id, value 0, 1, or 2

When GDPR does not apply, only gdprApplies, tcfPolicyVersion, cmpId and cmpVersion exist on the object — tcString and the purpose/vendor maps are absent by spec. Reading them without branching on gdprApplies throws or returns undefined.

The success boolean

Callback success is false when
addEventListener The callback could not be registered as a listener for any reason
removeEventListener The listener could not be removed (e.g. no registered listener matches listenerId)

Purposes (11)

# Purpose
1 Store and/or access information on a device
2 Use limited data to select advertising
3 Create profiles for personalised advertising
4 Use profiles to select personalised advertising
5 Create profiles to personalise content
6 Use profiles to select personalised content
7 Measure advertising performance
8 Measure content performance
9 Understand audiences through statistics or combinations of data from different sources
10 Develop and improve services
11 Use limited data to select content

Special purposes, features, special features

Special purposes (3)
1 Ensure security, prevent and detect fraud, and fix errors
2 Deliver and present advertising and content
3 Save and communicate privacy choices
Features (3)
1 Match and combine data from other data sources
2 Link different devices
3 Identify devices based on information transmitted automatically
Special features (2)
1 Use precise geolocation data
2 Actively scan device characteristics for identification

Special feature opt-ins are surfaced through specialFeatureOptins in TCData.

TCF v2.2 removed legitimate interest as an acceptable legal basis for the profiling and personalisation purposes. For these, evaluate purpose.consents only; ignore purpose.legitimateInterests.

Purpose Consent Legitimate interest
1 Yes No (consent-only)
2 Yes Yes
3 Yes No — removed in v2.2
4 Yes No — removed in v2.2
5 Yes No — removed in v2.2
6 Yes No — removed in v2.2
7–11 Yes Yes

Version numbers (do not confuse them)

Number Where it lives Value / rule
TC String Version field TC String core segment 2 for the v2.x string format
version argument __tcfapi(command, version, ...) 2 (API major version) for TCF v2.x
tcfPolicyVersion GVL, TCData, PingReturn 4 at v2.2 launch; incremented to 5 effective 4 Oct 2024; detect v2.2 as >= 4 (both are v2.2). Strings with policy < 4 are invalid after 30 Sept 2023
gvlSpecificationVersion GVL 3 (current v3 GVL)
vendorListVersion GVL and TC String Increments roughly weekly; 168 at time of fetch

Do not hardcode tcfPolicyVersion === 4. The TC String Version field (2) is a different number from tcfPolicyVersion (4 or 5).

Global Vendor List (v3) endpoints

Purpose URL
Latest v3 GVL https://vendor-list.consensu.org/v3/vendor-list.json
Versioned archive https://vendor-list.consensu.org/v3/archives/vendor-list-v{vendorListVersion}.json

The GVL JSON contains: gvlSpecificationVersion, vendorListVersion, tcfPolicyVersion, lastUpdated, purposes, specialPurposes, features, specialFeatures, stacks, dataCategories, and vendors.

Stub detection and cross-frame calls

Check for window.__tcfapi. For cross-frame access, locate an ancestor frame that has a child frame named __tcfapiLocator. The stub queues calls, creates the __tcfapiLocator iframe, and installs a postMessage handler; ping’s cmpLoaded / cmpStatus tell you whether the real CMP has replaced it.

postMessage shapes
// request
{ __tcfapiCall: { command, parameter, version, callId } }
// response
{ __tcfapiReturn: { returnValue, success, callId } }

callId is a unique id chosen by the caller to correlate the response.

First-layer UI requirements (v2.2)

Certified v2.2 CMPs must disclose, on the first layer of the UI, the number of third-party vendors seeking consent, and must make available standardized illustrations (based on real use cases) explaining how participants’ data processing relates to the purposes. Adopting a certified v2.2 CMP inherits these rather than coding them.

Gotchas

  1. getTCData returns nothing, or your CMP does not implement it. Cause: it was deprecated in v2.2 (May 2023) and is optional; only ping, addEventListener, removeEventListener are required. Fix: register an addEventListener callback and read tcData from it (it fires immediately with current data).
  2. tcData.tcString or purpose.consents is undefined for some users. Cause: when GDPR does not apply, the object holds only gdprApplies, tcfPolicyVersion, cmpId, cmpVersion. Fix: branch on gdprApplies first and never assume tcString exists.
  3. __tcfapi(...) calls are silently dropped on early page load. Cause: only the stub is active (cmpStatus stub, cmpLoaded false); the CMP decision is not ready. Fix: use addEventListener and wait for tcloaded or useractioncomplete, or poll ping until cmpLoaded / loaded.
  4. Pixels fire before the user actually chose. Cause: tcloaded means a valid (possibly pre-existing) TC String is available, not that the user interacted this session. Fix: gate new processing on useractioncomplete; use tcloaded only to honor a stored string.
  5. A tcfPolicyVersion === 4 check starts rejecting valid v2.2 strings. Cause: the policy was incremented to 5 (effective 4 Oct 2024); both 4 and 5 are v2.2. Fix: detect with ping.tcfPolicyVersion >= 4; treat policy < 4 strings as invalid.
  6. An addEventListener callback keeps firing and leaks. Cause: it is re-invoked on every TC String change until explicitly removed. Fix: capture tcData.listenerId and call removeEventListener with it (store the id and remove on a later tick — removing from inside the callback is not documented as safe).
  7. Personalised-ads vendors are wrongly allowed via legitimate interest. Cause: v2.2 removed LI as a legal basis for Purposes 3, 4, 5, 6. Fix: for Purposes 3–6 evaluate purpose.consents only; honor LI only for Purposes 2, 7, 8, 9, 10, 11 where declared.
  8. Server-side GVL fetch is rate-limited or serves stale data. Cause: not caching per the response max-age; unversioned latest changes under you. Fix: cache exactly as a browser would, and pin a versioned archive matching the VendorListVersion in the TC String for reproducibility.

Quick recipes

tcf-ready.js
// Wait for a usable consent state, then act. Works whether the CMP is still a
// stub or already loaded — the callback fires immediately with current data.
window.__tcfapi('addEventListener', 2, function (tcData, success) {
if (!success) return; // registration failed
if (tcData.gdprApplies === false) { // reduced object, no tcString
startTags(); // GDPR out of scope here
return;
}
if (tcData.eventStatus === 'useractioncomplete' ||
tcData.eventStatus === 'tcloaded') {
applyConsent(tcData); // honor the TC String
var listenerId = tcData.listenerId; // capture id; remove on a later tick
setTimeout(function () { // (spec does not document in-callback removal)
window.__tcfapi('removeEventListener', 2, function () {}, listenerId);
}, 0);
}
// 'cmpuishown' => UI is showing; wait for the user to confirm.
});
gate-vendor.js
// Purposes 3 & 4 are consent-only in v2.2 — do not read legitimateInterests.
const MY_VENDOR_ID = 755; // replace with your IAB-assigned vendor id
function canRunPersonalisedAds(tcData) {
const p = tcData.purpose.consents;
const v = tcData.vendor.consents;
return Boolean(p[1] && p[3] && p[4] && v[MY_VENDOR_ID]);
}
detect-v2-2.js
// tcfPolicyVersion is the pragmatic 'am I on v2.2?' probe.
window.__tcfapi('ping', 2, function (ping) {
const ready = ping.cmpLoaded && ping.cmpStatus === 'loaded';
const isV22 = typeof ping.tcfPolicyVersion === 'number' &&
ping.tcfPolicyVersion >= 4; // 4 or 5 are both v2.2
console.log({ ready, isV22, policy: ping.tcfPolicyVersion });
});
tcf-postmessage.js
// Call __tcfapi from a frame that does not host the CMP.
function callTcf(command, version, parameter, cb) {
const callId = String(Math.random());
function onMessage(event) {
let payload = event.data;
if (typeof payload === 'string') {
try { payload = JSON.parse(payload); } catch (e) { return; }
}
const r = payload && payload.__tcfapiReturn;
if (r && r.callId === callId) {
window.removeEventListener('message', onMessage);
cb(r.returnValue, r.success);
}
}
window.addEventListener('message', onMessage);
// Post to the frame owning the '__tcfapiLocator' child frame:
window.top.postMessage({ __tcfapiCall: { command, parameter, version, callId } }, '*');
}
callTcf('ping', 2, undefined, function (returnValue) { console.log(returnValue); });
fetch-gvl.js
// Prefer 'latest' with browser-style caching; pin an archive for reproducibility.
async function getGvl(vendorListVersion) {
const base = 'https://vendor-list.consensu.org/v3';
const url = vendorListVersion
? `${base}/archives/vendor-list-v${vendorListVersion}.json`
: `${base}/vendor-list.json`;
const res = await fetch(url); // honor the response max-age
return res.json(); // { purposes, vendors, tcfPolicyVersion, ... }
}

Sources

Official pages these values were read from (read 2026-07-20):

Honesty notes. gvlSpecificationVersion (3), vendorListVersion (168) and tcfPolicyVersion (5) are point-in-time values read from the live GVL on the verification date — vendorListVersion increments roughly weekly, so verify against the live GVL at build time rather than hardcoding. The publisher.restrictions value meanings (0, 1, 2 as restriction types) and the statement that Purpose 1 is consent-only reflect known TCF semantics; re-confirm the exact restriction-type enumeration against the consent-string format spec before relying on it. The spec does not explicitly state whether removeEventListener may be called from inside the addEventListener callback, so this card removes on a later tick. The loading cmpStatus is documented as deprecated but has no stated removal version, so some CMPs may still emit it.

Changelog

  • — Initial version, verified against official documentation.

dataLayer

0 events · 0 sent

    • home /
      writing /writing
      guides /guides
      work /#work
      about /about
      colophon /colophon
      toggle analyst mode ctrl+.
      print session receipt /#receipt