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(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.
Legal-basis matrix (v2.2 change)
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.
// 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
getTCDatareturns nothing, or your CMP does not implement it. Cause: it was deprecated in v2.2 (May 2023) and is optional; onlyping,addEventListener,removeEventListenerare required. Fix: register anaddEventListenercallback and readtcDatafrom it (it fires immediately with current data).tcData.tcStringorpurpose.consentsisundefinedfor some users. Cause: when GDPR does not apply, the object holds onlygdprApplies,tcfPolicyVersion,cmpId,cmpVersion. Fix: branch ongdprAppliesfirst and never assumetcStringexists.__tcfapi(...)calls are silently dropped on early page load. Cause: only the stub is active (cmpStatusstub,cmpLoadedfalse); the CMP decision is not ready. Fix: useaddEventListenerand wait fortcloadedoruseractioncomplete, or pollpinguntilcmpLoaded/loaded.- Pixels fire before the user actually chose. Cause:
tcloadedmeans a valid (possibly pre-existing) TC String is available, not that the user interacted this session. Fix: gate new processing onuseractioncomplete; usetcloadedonly to honor a stored string. - A
tcfPolicyVersion === 4check starts rejecting valid v2.2 strings. Cause: the policy was incremented to5(effective 4 Oct 2024); both4and5are v2.2. Fix: detect withping.tcfPolicyVersion >= 4; treat policy< 4strings as invalid. - An
addEventListenercallback keeps firing and leaks. Cause: it is re-invoked on every TC String change until explicitly removed. Fix: capturetcData.listenerIdand callremoveEventListenerwith it (store the id and remove on a later tick — removing from inside the callback is not documented as safe). - 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.consentsonly; honor LI only for Purposes 2, 7, 8, 9, 10, 11 where declared. - Server-side GVL fetch is rate-limited or serves stale data. Cause: not caching per the response
max-age; unversionedlatestchanges under you. Fix: cache exactly as a browser would, and pin a versioned archive matching theVendorListVersionin the TC String for reproducibility.
Quick recipes
// 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.});// Purposes 3 & 4 are consent-only in v2.2 — do not read legitimateInterests.const MY_VENDOR_ID = 755; // replace with your IAB-assigned vendor idfunction canRunPersonalisedAds(tcData) { const p = tcData.purpose.consents; const v = tcData.vendor.consents; return Boolean(p[1] && p[3] && p[4] && v[MY_VENDOR_ID]);}// 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 });});// 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); });// 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, ... }}Related
Sources
Official pages these values were read from (read 2026-07-20):
- IAB Tech Lab — CMP API v2 (TCF v2.2 technical spec) (read 2026-07-20)
- IAB Tech Lab — Consent string and vendor list formats v2 (read 2026-07-20)
- TCF v2.2 Global Vendor List (live GVL v3 JSON) (read 2026-07-20)
- Understanding the Transparency and Consent Framework v2.2 — IAB Europe (read 2026-07-20)
- FAQ: TCF v2.2 (updated November 2023) — IAB Europe (PDF) (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.