TLDR: IAB TCF 2.2 is the industry standard for ad tech consent management, encoding user choices into TC Strings that enable GDPR-compliant programmatic advertising.
Retour au blog
Compliance
Understanding IAB TCF 2.2 Framework: Complete Technical Guide
Marcus Weber, Compliance DirectorOctober 15, 202425 min de lecture
IAB TCFConsent FrameworkGDPRAdTech
---
slug: understanding-iab-tcf-2-2-framework
title: Understanding the IAB TCF 2.2 Framework
excerpt: A comprehensive technical guide to implementing the IAB Transparency and Consent Framework 2.2, including TC String generation, vendor management, and compliance requirements.
author: Marcus Thompson
published_at: 2024-11-15
category: Technical
tags:
- IAB TCF
- Technical Implementation
- Vendor Management
- Ad Tech
- GDPR Compliance
image_emoji: 🔧
seo_title: IAB TCF 2.2 Framework Implementation Guide - Complete Technical Documentation
seo_description: Master IAB TCF 2.2 implementation with this comprehensive guide covering TC String generation, vendor integration, consent signals, and GDPR compliance requirements.
read_time_minutes: 42
faq:
- question: What is the IAB Transparency and Consent Framework?
answer: The IAB TCF is a standardized framework for obtaining and transmitting user consent for digital advertising. It provides a common format (TC String) for communicating consent choices across the ad tech ecosystem, ensuring GDPR compliance while enabling programmatic advertising.
- question: What changed in TCF 2.2 from previous versions?
answer: TCF 2.2 introduced stricter requirements following regulatory rulings, including removal of legitimate interest for certain purposes (like basic ad serving), enhanced transparency requirements, and improved vendor accountability. It also added support for Google's Additional Consent mode for non-TCF vendors.
- question: Do I need to implement TCF if I'm not in advertising?
answer: No. TCF is specifically designed for digital advertising ecosystems. If you're not serving ads or sharing data with ad tech vendors, a simpler consent management approach is more appropriate. TCF adds significant complexity that's only necessary for programmatic advertising.
- question: How do I generate a valid TC String?
answer: A TC String encodes user consent choices in a binary format following IAB specifications. You can use IAB-approved libraries (like @iabtcf/core) or build your own encoder. The string includes consent data, vendor approvals, special feature opt-ins, and metadata like timestamps and version numbers.
- question: Can I use TCF 2.2 with Google Consent Mode v2?
answer: Yes. Google Consent Mode v2 can work alongside TCF 2.2. The TC String communicates consent to TCF-registered vendors, while Consent Mode signals control Google's own tags. Many CMPs support both simultaneously, translating user choices into both formats.
---
## What is the IAB TCF 2.2 Framework and Why Does It Matter?
The Interactive Advertising Bureau (IAB) Transparency and Consent Framework (TCF) is the advertising industry's response to GDPR requirements for lawful data processing in programmatic advertising. Version 2.2, released following significant regulatory challenges to earlier versions, represents the current standard for managing consent in the complex, multi-party ecosystem of digital advertising.
**Why TCF exists:**
Digital advertising involves dozens or hundreds of vendors—ad servers, SSPs, DSPs, DMPs, analytics providers, and more—each processing personal data. Without a standardized format for consent, each vendor would need to collect consent independently, creating a terrible user experience and fragmented compliance. TCF solves this by:
1. Providing a single point of consent collection
2. Encoding consent choices in a compact, standardized format (TC String)
3. Defining standard purposes and special features
4. Maintaining a Global Vendor List of registered participants
5. Enabling consent signal transmission across the entire ad tech chain
**The evolution to 2.2:**
- **TCF 1.0** (2018): Initial framework, basic consent strings
- **TCF 2.0** (2019): Major overhaul, introduced legitimate interest options
- **TCF 2.1** (2020): Bug fixes and clarifications
- **TCF 2.2** (2022): Post-Belgian DPA ruling updates, removed legitimate interest from certain purposes, enhanced transparency
The Belgian Data Protection Authority's 2022 ruling against IAB Europe forced significant changes, particularly around legitimate interest claims and vendor accountability. TCF 2.2 reflects these regulatory learnings.
## TCF Architecture and Components
Understanding TCF requires grasping its core components and how they interact:
```typescript
// tcf-architecture.ts
// Core TCF 2.2 architecture components
/**
* TCF Core Components Overview
*/
interface TCFArchitecture {
cmp: ConsentManagementPlatform;
gvl: GlobalVendorList;
tcString: TCString;
cmpApi: TCFAPIInterface;
vendorList: VendorConfiguration;
publisherRestrictions: PublisherRestrictions;
}
/**
* Global Vendor List - The registry of all TCF-registered vendors
*/
interface GlobalVendorList {
gvlSpecificationVersion: number; // Current: 2
vendorListVersion: number; // Increments with each update
tcfPolicyVersion: number; // Current TCF policy version: 4
lastUpdated: string; // ISO date of last update
purposes: Map;
specialPurposes: Map;
features: Map;
specialFeatures: Map;
stacks: Map;
vendors: Map;
}
/**
* TCF Purposes - What vendors can do with data
*/
interface Purpose {
id: number;
name: string;
description: string;
descriptionLegal: string;
consentable: boolean;
rightToObject: boolean;
}
/**
* TCF 2.2 Standard Purposes
*/
const TCF_PURPOSES: Record = {
1: {
id: 1,
name: "Store and/or access information on a device",
description: "Cookies, device identifiers, or other information can be stored or accessed on your device for the purposes presented to you.",
descriptionLegal: "Vendors can: Store and access information on the device such as cookies and device identifiers presented to a user.",
consentable: true,
rightToObject: false
},
2: {
id: 2,
name: "Use limited data to select advertising",
description: "Advertising presented to you on this service can be based on limited data, such as the website or app you are using, your non-precise location, your device type or which content you are interacting with.",
descriptionLegal: "Advertising can be presented based on limited data. This means advertising cannot be personalised on the individual but can use general aggregated data.",
consentable: true,
rightToObject: false
},
3: {
id: 3,
name: "Create profiles for personalised advertising",
description: "Information about your activity on this service can be used to build or improve a profile about you for personalised advertising.",
descriptionLegal: "Information about your activity on this service, such as your interaction with ads or content, can be very helpful to improve products and services and to build new products and services based on user interactions, the type of audience, etc.",
consentable: true,
rightToObject: true
},
4: {
id: 4,
name: "Use profiles to select personalised advertising",
description: "Personalised advertising can be shown to you based on a profile about you.",
descriptionLegal: "Advertising presented to you on this service can be based on your advertising profiles, which can reflect your activity on this service or other websites or apps, possible interests and personal aspects.",
consentable: true,
rightToObject: true
},
5: {
id: 5,
name: "Create profiles to personalise content",
description: "Information about your activity on this service can be used to build or improve a profile about you for personalised content.",
descriptionLegal: "Information about your activity on this service can be used to build or improve a profile about you to personalise content.",
consentable: true,
rightToObject: true
},
6: {
id: 6,
name: "Use profiles to select personalised content",
description: "Personalised content can be shown to you based on a profile about you.",
descriptionLegal: "Content presented to you on this service can be based on your content personalisation profiles.",
consentable: true,
rightToObject: true
},
7: {
id: 7,
name: "Measure advertising performance",
description: "The performance and effectiveness of ads can be measured.",
descriptionLegal: "Information regarding which advertising is presented to you and how you interact with it can be used to determine how well an advert has worked for you or other users.",
consentable: true,
rightToObject: true
},
8: {
id: 8,
name: "Measure content performance",
description: "The performance and effectiveness of content can be measured.",
descriptionLegal: "Information regarding which content is presented to you and how you interact with it can be used to determine whether the content reached its intended audience.",
consentable: true,
rightToObject: true
},
9: {
id: 9,
name: "Understand audiences through statistics or combinations of data",
description: "Market research can be used to learn more about the audiences who visit sites/apps and view ads.",
descriptionLegal: "Reports can be generated based on the combination of data sets regarding your interactions with advertising or content.",
consentable: true,
rightToObject: true
},
10: {
id: 10,
name: "Develop and improve services",
description: "Information about your activity on this service can be used to improve it.",
descriptionLegal: "Information about your activity on this service, such as your interaction with ads or content, can be very helpful to improve products and services.",
consentable: true,
rightToObject: true
},
11: {
id: 11,
name: "Use limited data to select content",
description: "Content presented to you on this service can be based on limited data, such as the website or app you are using, your non-precise location, your device type, or which content you are interacting with.",
descriptionLegal: "Content can be presented based on limited data without creating profiles or measuring effectiveness.",
consentable: true,
rightToObject: false
}
};
/**
* Special Purposes - Cannot be objected to
*/
const SPECIAL_PURPOSES: Record = {
1: {
id: 1,
name: "Ensure security, prevent and detect fraud, and fix errors",
description: "Your data can be used to monitor for and prevent unusual and possibly fraudulent activity.",
descriptionLegal: "Vendors can ensure security, prevent and detect fraud, and fix errors.",
consentable: false,
rightToObject: false
},
2: {
id: 2,
name: "Deliver and present advertising and content",
description: "Certain information can be used to deliver advertising and content.",
descriptionLegal: "Vendors can deliver and present advertising and content based on technical delivery needs.",
consentable: false,
rightToObject: false
}
};
/**
* Special Features - Require explicit opt-in
*/
const SPECIAL_FEATURES: Record = {
1: {
id: 1,
name: "Use precise geolocation data",
description: "Your precise geolocation data can be used in support of one or more purposes.",
descriptionLegal: "With your acceptance, your precise location (within a radius of less than 500 metres) may be used.",
requiresOptIn: true
},
2: {
id: 2,
name: "Actively scan device characteristics for identification",
description: "Your device can be identified based on a scan of your device's unique combination of characteristics.",
descriptionLegal: "With your acceptance, certain characteristics specific to your device might be requested and used to distinguish it from other devices.",
requiresOptIn: true
}
};
/**
* Vendor - A company registered in the Global Vendor List
*/
interface Vendor {
id: number;
name: string;
purposes: number[]; // Purposes vendor declares for consent
legIntPurposes: number[]; // Purposes vendor claims legitimate interest
flexiblePurposes: number[]; // Purposes that can be either
specialPurposes: number[]; // Special purposes vendor uses
features: number[]; // Features vendor uses
specialFeatures: number[]; // Special features requiring opt-in
policyUrl: string; // Privacy policy URL
deletedDate?: string; // If vendor was removed from GVL
overflow?: { // Overflow options for vendors
httpGetLimit: number;
};
}
/**
* TC String - The encoded consent data
*/
interface TCString {
// Core segment (always present)
version: number; // TCF version (2)
created: Date; // When consent was first created
lastUpdated: Date; // When consent was last updated
cmpId: number; // CMP that created the string
cmpVersion: number; // Version of the CMP
consentScreen: number; // Screen number where consent was given
consentLanguage: string; // ISO 639-1 language code
vendorListVersion: number; // GVL version used
tcfPolicyVersion: number; // TCF policy version
isServiceSpecific: boolean; // Publisher-specific or global
useNonStandardStacks: boolean; // Custom stack usage
specialFeatureOptIns: BitField; // Special features the user opted into
purposesConsent: BitField; // Purposes user consented to
purposesLITransparency: BitField; // Purposes disclosed for LI
purposeOneTreatment: boolean; // Special treatment for purpose 1
publisherCC: string; // Publisher's country code
// Vendor consents
maxVendorId: number;
isRangeEncoding: boolean;
vendorConsents: BitField | VendorRange[];
vendorLegitimateInterests: BitField | VendorRange[];
// Optional segments
disclosedVendors?: DisclosedVendorsSegment;
allowedVendors?: AllowedVendorsSegment;
publisherTC?: PublisherTCSegment;
}
/**
* Publisher Restrictions - Publishers can restrict vendor purposes
*/
interface PublisherRestrictions {
[purposeId: number]: {
[vendorId: number]: RestrictionType;
};
}
enum RestrictionType {
NotAllowed = 0, // Purpose completely disallowed for vendor
RequireConsent = 1, // Require consent (no LI)
RequireLegitimateInterest = 2 // Require LI (no consent)
}
/**
* CMP API - JavaScript API for interacting with TCF
*/
interface TCFAPIInterface {
// Main API method
__tcfapi(
command: TCFCommand,
version: number,
callback: TCFCallback,
parameter?: any
): void;
}
type TCFCommand =
| 'ping'
| 'getTCData'
| 'getVendorList'
| 'getInAppTCData'
| 'addEventListener'
| 'removeEventListener';
interface TCFCallback {
(data: TCData, success: boolean): void;
}
interface TCData {
tcString: string;
tcfPolicyVersion: number;
cmpId: number;
cmpVersion: number;
gdprApplies: boolean;
eventStatus: 'tcloaded' | 'cmpuishown' | 'useractioncomplete';
cmpStatus: 'stub' | 'loading' | 'loaded' | 'error';
listenerId?: number;
isServiceSpecific: boolean;
useNonStandardStacks: boolean;
publisherCC: string;
purposeOneTreatment: boolean;
outOfBand: {
allowedVendors: number[];
disclosedVendors: number[];
};
purpose: {
consents: { [key: number]: boolean };
legitimateInterests: { [key: number]: boolean };
};
vendor: {
consents: { [key: number]: boolean };
legitimateInterests: { [key: number]: boolean };
};
specialFeatureOptins: { [key: number]: boolean };
publisher: {
consents: { [key: number]: boolean };
legitimateInterests: { [key: number]: boolean };
customPurpose: {
consents: { [key: number]: boolean };
legitimateInterests: { [key: number]: boolean };
};
restrictions: { [key: number]: { [key: number]: number } };
};
}
interface SpecialPurpose {
id: number;
name: string;
description: string;
descriptionLegal: string;
consentable: boolean;
rightToObject: boolean;
}
interface Feature {
id: number;
name: string;
description: string;
descriptionLegal: string;
}
interface SpecialFeature {
id: number;
name: string;
description: string;
descriptionLegal: string;
requiresOptIn: boolean;
}
interface Stack {
id: number;
name: string;
description: string;
purposes: number[];
specialFeatures: number[];
}
type BitField = boolean[];
interface VendorRange {
isRange: boolean;
startOrOnlyVendorId: number;
endVendorId?: number;
}
interface DisclosedVendorsSegment {
disclosedVendors: BitField | VendorRange[];
}
interface AllowedVendorsSegment {
allowedVendors: BitField | VendorRange[];
}
interface PublisherTCSegment {
pubPurposesConsent: BitField;
pubPurposesLITransparency: BitField;
customPurposesConsent?: BitField;
customPurposesLITransparency?: BitField;
}
export {
TCFArchitecture,
GlobalVendorList,
TCString,
Vendor,
TCData,
TCFAPIInterface,
PublisherRestrictions,
RestrictionType,
TCF_PURPOSES,
SPECIAL_PURPOSES,
SPECIAL_FEATURES
};
```
## Implementing TC String Encoding and Decoding
The TC String is the heart of TCF—a compact binary representation of consent choices. Here's a complete implementation:
```typescript
// tc-string-encoder.ts
// Complete TC String encoder/decoder for TCF 2.2
class TCStringEncoder {
private static readonly ENCODING_VERSION = 2;
private static readonly CORE_SEGMENT_VERSION = 2;
/**
* Encode consent data into a TC String
*/
static encode(data: TCStringData): string {
const segments: string[] = [];
// Core segment (required)
const coreSegment = this.encodeCoreSegment(data);
segments.push(coreSegment);
// Disclosed vendors segment (optional but recommended)
if (data.disclosedVendors && data.disclosedVendors.length > 0) {
segments.push(this.encodeDisclosedVendorsSegment(data.disclosedVendors));
}
// Allowed vendors segment (for service-specific strings)
if (data.isServiceSpecific && data.allowedVendors && data.allowedVendors.length > 0) {
segments.push(this.encodeAllowedVendorsSegment(data.allowedVendors));
}
// Publisher TC segment (publisher purposes)
if (data.publisherPurposes) {
segments.push(this.encodePublisherTCSegment(data.publisherPurposes));
}
return segments.join('.');
}
/**
* Encode the core segment
*/
private static encodeCoreSegment(data: TCStringData): string {
const bits: string[] = [];
// Version (6 bits) - always 2 for TCF 2.x
bits.push(this.intToBits(this.CORE_SEGMENT_VERSION, 6));
// Created timestamp (36 bits) - deciseconds since epoch
bits.push(this.dateToBits(data.created));
// Last updated timestamp (36 bits)
bits.push(this.dateToBits(data.lastUpdated));
// CMP ID (12 bits)
bits.push(this.intToBits(data.cmpId, 12));
// CMP version (12 bits)
bits.push(this.intToBits(data.cmpVersion, 12));
// Consent screen (6 bits)
bits.push(this.intToBits(data.consentScreen, 6));
// Consent language (12 bits) - two 6-bit chars
bits.push(this.langToBits(data.consentLanguage));
// Vendor list version (12 bits)
bits.push(this.intToBits(data.vendorListVersion, 12));
// TCF policy version (6 bits)
bits.push(this.intToBits(data.tcfPolicyVersion, 6));
// Is service specific (1 bit)
bits.push(data.isServiceSpecific ? '1' : '0');
// Use non-standard stacks (1 bit)
bits.push(data.useNonStandardStacks ? '1' : '0');
// Special feature opt-ins (12 bits - one per special feature)
bits.push(this.bitFieldToBits(data.specialFeatureOptIns, 12));
// Purposes consent (24 bits - one per purpose)
bits.push(this.bitFieldToBits(data.purposesConsent, 24));
// Purposes legitimate interest transparency (24 bits)
bits.push(this.bitFieldToBits(data.purposesLITransparency, 24));
// Purpose one treatment (1 bit)
bits.push(data.purposeOneTreatment ? '1' : '0');
// Publisher country code (12 bits)
bits.push(this.langToBits(data.publisherCC));
// Vendor consents section
const maxVendorId = Math.max(...data.vendorConsents.map(v => v.id));
bits.push(this.intToBits(maxVendorId, 16));
// Encode vendor consents (BitField or Range encoding)
const vendorConsentBits = this.encodeVendorSection(
data.vendorConsents,
maxVendorId
);
bits.push(vendorConsentBits);
// Vendor legitimate interests section
const vendorLIBits = this.encodeVendorSection(
data.vendorLegitimateInterests,
maxVendorId
);
bits.push(vendorLIBits);
// Publisher restrictions
bits.push(this.encodePublisherRestrictions(data.publisherRestrictions));
// Convert bit string to base64url
const bitString = bits.join('');
return this.bitsToBase64Url(bitString);
}
/**
* Encode vendor consent/LI section
*/
private static encodeVendorSection(
vendors: VendorConsent[],
maxVendorId: number
): string {
// Decide whether to use BitField or Range encoding
const useRangeEncoding = this.shouldUseRangeEncoding(vendors, maxVendorId);
const bits: string[] = [];
bits.push(useRangeEncoding ? '1' : '0');
if (useRangeEncoding) {
// Range encoding
const ranges = this.convertToRanges(vendors);
bits.push(this.intToBits(ranges.length, 12));
ranges.forEach(range => {
bits.push(range.isRange ? '1' : '0');
bits.push(this.intToBits(range.start, 16));
if (range.isRange) {
bits.push(this.intToBits(range.end!, 16));
}
});
} else {
// BitField encoding
const bitField = new Array(maxVendorId).fill(false);
vendors.forEach(v => {
if (v.hasConsent) bitField[v.id - 1] = true;
});
bits.push(bitField.map(b => b ? '1' : '0').join(''));
}
return bits.join('');
}
/**
* Encode publisher restrictions
*/
private static encodePublisherRestrictions(
restrictions: PublisherRestriction[]
): string {
const bits: string[] = [];
// Number of restrictions (12 bits)
bits.push(this.intToBits(restrictions.length, 12));
restrictions.forEach(restriction => {
// Purpose ID (6 bits)
bits.push(this.intToBits(restriction.purposeId, 6));
// Restriction type (2 bits)
bits.push(this.intToBits(restriction.restrictionType, 2));
// Vendor ranges
const ranges = this.convertToRanges(restriction.vendors);
bits.push(this.intToBits(ranges.length, 12));
ranges.forEach(range => {
bits.push(range.isRange ? '1' : '0');
bits.push(this.intToBits(range.start, 16));
if (range.isRange) {
bits.push(this.intToBits(range.end!, 16));
}
});
});
return bits.join('');
}
/**
* Decode a TC String back to data
*/
static decode(tcString: string): TCStringData {
const segments = tcString.split('.');
const coreSegment = segments[0];
// Decode core segment
const bits = this.base64UrlToBits(coreSegment);
let offset = 0;
const data: TCStringData = {
version: this.bitsToInt(bits.substr(offset, 6)),
created: new Date(),
lastUpdated: new Date(),
cmpId: 0,
cmpVersion: 0,
consentScreen: 0,
consentLanguage: '',
vendorListVersion: 0,
tcfPolicyVersion: 0,
isServiceSpecific: false,
useNonStandardStacks: false,
specialFeatureOptIns: [],
purposesConsent: [],
purposesLITransparency: [],
purposeOneTreatment: false,
publisherCC: '',
vendorConsents: [],
vendorLegitimateInterests: [],
publisherRestrictions: []
};
offset += 6;
// Created (36 bits)
data.created = this.bitsToDate(bits.substr(offset, 36));
offset += 36;
// Last updated (36 bits)
data.lastUpdated = this.bitsToDate(bits.substr(offset, 36));
offset += 36;
// CMP ID (12 bits)
data.cmpId = this.bitsToInt(bits.substr(offset, 12));
offset += 12;
// CMP version (12 bits)
data.cmpVersion = this.bitsToInt(bits.substr(offset, 12));
offset += 12;
// Consent screen (6 bits)
data.consentScreen = this.bitsToInt(bits.substr(offset, 6));
offset += 6;
// Consent language (12 bits)
data.consentLanguage = this.bitsToLang(bits.substr(offset, 12));
offset += 12;
// Vendor list version (12 bits)
data.vendorListVersion = this.bitsToInt(bits.substr(offset, 12));
offset += 12;
// TCF policy version (6 bits)
data.tcfPolicyVersion = this.bitsToInt(bits.substr(offset, 6));
offset += 6;
// Is service specific (1 bit)
data.isServiceSpecific = bits[offset] === '1';
offset += 1;
// Use non-standard stacks (1 bit)
data.useNonStandardStacks = bits[offset] === '1';
offset += 1;
// Special feature opt-ins (12 bits)
data.specialFeatureOptIns = this.bitsToBitField(bits.substr(offset, 12));
offset += 12;
// Purposes consent (24 bits)
data.purposesConsent = this.bitsToBitField(bits.substr(offset, 24));
offset += 24;
// Purposes LI transparency (24 bits)
data.purposesLITransparency = this.bitsToBitField(bits.substr(offset, 24));
offset += 24;
// Purpose one treatment (1 bit)
data.purposeOneTreatment = bits[offset] === '1';
offset += 1;
// Publisher CC (12 bits)
data.publisherCC = this.bitsToLang(bits.substr(offset, 12));
offset += 12;
// Continue decoding vendor sections...
// (Implementation continues with vendor consents, LI, and restrictions)
return data;
}
// Helper methods
private static intToBits(value: number, length: number): string {
return value.toString(2).padStart(length, '0');
}
private static bitsToInt(bits: string): number {
return parseInt(bits, 2);
}
private static dateToBits(date: Date): string {
const deciseconds = Math.floor(date.getTime() / 100);
return this.intToBits(deciseconds, 36);
}
private static bitsToDate(bits: string): Date {
const deciseconds = this.bitsToInt(bits);
return new Date(deciseconds * 100);
}
private static langToBits(lang: string): string {
const upper = lang.toUpperCase();
const char1 = upper.charCodeAt(0) - 65; // A=0
const char2 = upper.charCodeAt(1) - 65;
return this.intToBits(char1, 6) + this.intToBits(char2, 6);
}
private static bitsToLang(bits: string): string {
const char1 = String.fromCharCode(this.bitsToInt(bits.substr(0, 6)) + 65);
const char2 = String.fromCharCode(this.bitsToInt(bits.substr(6, 6)) + 65);
return (char1 + char2).toLowerCase();
}
private static bitFieldToBits(bitField: boolean[], length: number): string {
return bitField.slice(0, length).map(b => b ? '1' : '0').join('');
}
private static bitsToBitField(bits: string): boolean[] {
return bits.split('').map(b => b === '1');
}
private static bitsToBase64Url(bits: string): string {
// Pad to multiple of 6 (base64 encoding)
const padded = bits.padEnd(Math.ceil(bits.length / 6) * 6, '0');
// Convert to bytes then base64
const bytes: number[] = [];
for (let i = 0; i < padded.length; i += 8) {
bytes.push(parseInt(padded.substr(i, 8), 2));
}
const base64 = btoa(String.fromCharCode(...bytes));
// Convert to base64url (replace + with -, / with _, remove =)
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
private static base64UrlToBits(base64url: string): string {
// Convert base64url to base64
const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
// Decode
const binary = atob(base64);
// Convert to bit string
return Array.from(binary)
.map(char => char.charCodeAt(0).toString(2).padStart(8, '0'))
.join('');
}
private static shouldUseRangeEncoding(
vendors: VendorConsent[],
maxVendorId: number
): boolean {
// Use range encoding if it saves space
const bitFieldSize = maxVendorId;
const ranges = this.convertToRanges(vendors);
const rangeSize = 12 + ranges.reduce((sum, r) =>
sum + 1 + 16 + (r.isRange ? 16 : 0), 0
);
return rangeSize < bitFieldSize;
}
private static convertToRanges(
vendors: VendorConsent[]
): VendorRange[] {
if (vendors.length === 0) return [];
const sorted = [...vendors].sort((a, b) => a.id - b.id);
const ranges: VendorRange[] = [];
let rangeStart = sorted[0].id;
let rangeEnd = sorted[0].id;
for (let i = 1; i < sorted.length; i++) {
if (sorted[i].id === rangeEnd + 1) {
rangeEnd = sorted[i].id;
} else {
ranges.push({
isRange: rangeStart !== rangeEnd,
start: rangeStart,
end: rangeStart !== rangeEnd ? rangeEnd : undefined
});
rangeStart = sorted[i].id;
rangeEnd = sorted[i].id;
}
}
ranges.push({
isRange: rangeStart !== rangeEnd,
start: rangeStart,
end: rangeStart !== rangeEnd ? rangeEnd : undefined
});
return ranges;
}
private static encodeDisclosedVendorsSegment(vendors: number[]): string {
// Type = 1 for disclosed vendors
const bits: string[] = [];
bits.push(this.intToBits(1, 3));
const maxVendorId = Math.max(...vendors);
bits.push(this.intToBits(maxVendorId, 16));
const vendorConsents = vendors.map(id => ({ id, hasConsent: true }));
bits.push(this.encodeVendorSection(vendorConsents, maxVendorId));
return this.bitsToBase64Url(bits.join(''));
}
private static encodeAllowedVendorsSegment(vendors: number[]): string {
// Type = 2 for allowed vendors
const bits: string[] = [];
bits.push(this.intToBits(2, 3));
const maxVendorId = Math.max(...vendors);
bits.push(this.intToBits(maxVendorId, 16));
const vendorConsents = vendors.map(id => ({ id, hasConsent: true }));
bits.push(this.encodeVendorSection(vendorConsents, maxVendorId));
return this.bitsToBase64Url(bits.join(''));
}
private static encodePublisherTCSegment(purposes: PublisherPurposes): string {
// Type = 3 for publisher TC
const bits: string[] = [];
bits.push(this.intToBits(3, 3));
bits.push(this.bitFieldToBits(purposes.consents, 24));
bits.push(this.bitFieldToBits(purposes.legitimateInterests, 24));
if (purposes.customPurposes) {
bits.push(this.intToBits(purposes.customPurposes.consents.length, 6));
bits.push(purposes.customPurposes.consents.map(b => b ? '1' : '0').join(''));
bits.push(purposes.customPurposes.legitimateInterests.map(b => b ? '1' : '0').join(''));
}
return this.bitsToBase64Url(bits.join(''));
}
}
interface TCStringData {
version: number;
created: Date;
lastUpdated: Date;
cmpId: number;
cmpVersion: number;
consentScreen: number;
consentLanguage: string;
vendorListVersion: number;
tcfPolicyVersion: number;
isServiceSpecific: boolean;
useNonStandardStacks: boolean;
specialFeatureOptIns: boolean[];
purposesConsent: boolean[];
purposesLITransparency: boolean[];
purposeOneTreatment: boolean;
publisherCC: string;
vendorConsents: VendorConsent[];
vendorLegitimateInterests: VendorConsent[];
publisherRestrictions: PublisherRestriction[];
disclosedVendors?: number[];
allowedVendors?: number[];
publisherPurposes?: PublisherPurposes;
}
interface VendorConsent {
id: number;
hasConsent: boolean;
}
interface VendorRange {
isRange: boolean;
start: number;
end?: number;
}
interface PublisherRestriction {
purposeId: number;
restrictionType: number;
vendors: VendorConsent[];
}
interface PublisherPurposes {
consents: boolean[];
legitimateInterests: boolean[];
customPurposes?: {
consents: boolean[];
legitimateInterests: boolean[];
};
}
export { TCStringEncoder, TCStringData };
```
## Implementing the CMP API
The `__tcfapi` function is how vendors and scripts query consent state:
```typescript
// tcf-api-implementation.ts
// Complete CMP API implementation for TCF 2.2
class TCFAPIImplementation {
private tcData: TCData | null = null;
private eventListeners: Map = new Map();
private nextListenerId = 1;
private cmpStatus: 'stub' | 'loading' | 'loaded' | 'error' = 'loading';
private eventStatus: 'tcloaded' | 'cmpuishown' | 'useractioncomplete' = 'tcloaded';
constructor(
private cmpId: number,
private cmpVersion: number,
private gvl: GlobalVendorList
) {
this.initializeAPI();
}
/**
* Initialize the __tcfapi function on window
*/
private initializeAPI(): void {
const self = this;
(window as any).__tcfapi = function(
command: string,
version: number,
callback: Function,
parameter?: any
): void {
if (typeof callback !== 'function') {
console.error('TCF API: callback must be a function');
return;
}
if (version !== 2) {
callback(null, false);
return;
}
try {
switch (command) {
case 'ping':
self.handlePing(callback);
break;
case 'getTCData':
self.handleGetTCData(callback, parameter);
break;
case 'getVendorList':
self.handleGetVendorList(callback, parameter);
break;
case 'addEventListener':
self.handleAddEventListener(callback);
break;
case 'removeEventListener':
self.handleRemoveEventListener(callback, parameter);
break;
default:
callback(null, false);
}
} catch (error) {
console.error('TCF API error:', error);
callback(null, false);
}
};
}
/**
* Handle ping command - returns CMP status
*/
private handlePing(callback: Function): void {
const pingReturn: PingReturn = {
gdprApplies: true,
cmpLoaded: this.cmpStatus === 'loaded',
cmpStatus: this.cmpStatus,
displayStatus: this.getDisplayStatus(),
apiVersion: '2.2',
cmpVersion: this.cmpVersion,
cmpId: this.cmpId,
gvlVersion: this.gvl.vendorListVersion,
tcfPolicyVersion: this.gvl.tcfPolicyVersion
};
callback(pingReturn, true);
}
/**
* Handle getTCData - returns current consent data
*/
private handleGetTCData(callback: Function, vendorIds?: number[]): void {
if (!this.tcData) {
// No consent data yet
callback(
{
gdprApplies: true,
tcfPolicyVersion: this.gvl.tcfPolicyVersion,
cmpId: this.cmpId,
cmpVersion: this.cmpVersion,
cmpStatus: this.cmpStatus,
eventStatus: this.eventStatus
},
false
);
return;
}
// Filter to requested vendors if specified
let tcData = this.tcData;
if (vendorIds && vendorIds.length > 0) {
tcData = this.filterTCDataForVendors(tcData, vendorIds);
}
callback(tcData, true);
}
/**
* Handle getVendorList - returns GVL or specific vendor info
*/
private handleGetVendorList(callback: Function, vendorListVersion?: number): void {
// If version specified, would need to fetch that specific version
// For now, return current GVL
callback(this.gvl, true);
}
/**
* Handle addEventListener - register for consent change events
*/
private handleAddEventListener(callback: Function): void {
const listenerId = this.nextListenerId++;
const listener: EventListener = {
id: listenerId,
callback
};
this.eventListeners.set(listenerId, listener);
// Immediately call with current data
if (this.tcData) {
const tcDataWithListener = {
...this.tcData,
listenerId
};
callback(tcDataWithListener, true);
}
}
/**
* Handle removeEventListener
*/
private handleRemoveEventListener(callback: Function, listenerId?: number): void {
if (listenerId) {
const removed = this.eventListeners.delete(listenerId);
callback(true, removed);
} else {
callback(false, false);
}
}
/**
* Update TC data and notify listeners
*/
updateTCData(
tcString: string,
vendorConsents: Map,
vendorLegitimateInterests: Map,
purposeConsents: Map,
purposeLegitimateInterests: Map,
specialFeatureOptIns: Map,
publisherRestrictions: Map>
): void {
const decodedTC = TCStringEncoder.decode(tcString);
this.tcData = {
tcString,
tcfPolicyVersion: decodedTC.tcfPolicyVersion,
cmpId: this.cmpId,
cmpVersion: this.cmpVersion,
gdprApplies: true,
eventStatus: this.eventStatus,
cmpStatus: this.cmpStatus,
isServiceSpecific: decodedTC.isServiceSpecific,
useNonStandardStacks: decodedTC.useNonStandardStacks,
publisherCC: decodedTC.publisherCC,
purposeOneTreatment: decodedTC.purposeOneTreatment,
outOfBand: {
allowedVendors: decodedTC.allowedVendors || [],
disclosedVendors: decodedTC.disclosedVendors || []
},
purpose: {
consents: this.mapToObject(purposeConsents),
legitimateInterests: this.mapToObject(purposeLegitimateInterests)
},
vendor: {
consents: this.mapToObject(vendorConsents),
legitimateInterests: this.mapToObject(vendorLegitimateInterests)
},
specialFeatureOptins: this.mapToObject(specialFeatureOptIns),
publisher: {
consents: {},
legitimateInterests: {},
customPurpose: {
consents: {},
legitimateInterests: {}
},
restrictions: this.convertPublisherRestrictions(publisherRestrictions)
}
};
// Notify all event listeners
this.notifyListeners();
}
/**
* Notify all registered event listeners
*/
private notifyListeners(): void {
if (!this.tcData) return;
this.eventListeners.forEach((listener, listenerId) => {
const tcDataWithListener = {
...this.tcData!,
listenerId
};
try {
listener.callback(tcDataWithListener, true);
} catch (error) {
console.error(`Error calling listener ${listenerId}:`, error);
}
});
}
/**
* Set event status and notify listeners
*/
setEventStatus(status: 'tcloaded' | 'cmpuishown' | 'useractioncomplete'): void {
this.eventStatus = status;
if (this.tcData) {
this.tcData.eventStatus = status;
this.notifyListeners();
}
}
/**
* Set CMP status
*/
setCMPStatus(status: 'stub' | 'loading' | 'loaded' | 'error'): void {
this.cmpStatus = status;
if (this.tcData) {
this.tcData.cmpStatus = status;
}
}
/**
* Helper methods
*/
private getDisplayStatus(): 'hidden' | 'visible' | 'disabled' {
if (this.eventStatus === 'cmpuishown') return 'visible';
if (this.cmpStatus === 'loaded') return 'hidden';
return 'disabled';
}
private filterTCDataForVendors(tcData: TCData, vendorIds: number[]): TCData {
const filtered = { ...tcData };
// Filter vendor consents
const filteredVendorConsents: { [key: number]: boolean } = {};
vendorIds.forEach(id => {
if (id in tcData.vendor.consents) {
filteredVendorConsents[id] = tcData.vendor.consents[id];
}
});
filtered.vendor.consents = filteredVendorConsents;
// Filter vendor LI
const filteredVendorLI: { [key: number]: boolean } = {};
vendorIds.forEach(id => {
if (id in tcData.vendor.legitimateInterests) {
filteredVendorLI[id] = tcData.vendor.legitimateInterests[id];
}
});
filtered.vendor.legitimateInterests = filteredVendorLI;
return filtered;
}
private mapToObject(map: Map): { [key: number]: boolean } {
const obj: { [key: number]: boolean } = {};
map.forEach((value, key) => {
obj[key] = value;
});
return obj;
}
private convertPublisherRestrictions(
restrictions: Map>
): { [purposeId: number]: { [vendorId: number]: number } } {
const result: { [purposeId: number]: { [vendorId: number]: number } } = {};
restrictions.forEach((vendorMap, purposeId) => {
result[purposeId] = {};
vendorMap.forEach((restrictionType, vendorId) => {
result[purposeId][vendorId] = restrictionType;
});
});
return result;
}
}
interface PingReturn {
gdprApplies: boolean;
cmpLoaded: boolean;
cmpStatus: string;
displayStatus: string;
apiVersion: string;
cmpVersion: number;
cmpId: number;
gvlVersion: number;
tcfPolicyVersion: number;
}
interface EventListener {
id: number;
callback: Function;
}
export { TCFAPIImplementation };
```
## Best Practices and Common Pitfalls
### Critical Implementation Requirements
1. **Always use the latest GVL**: Vendor lists update frequently. Cache for performance but refresh daily.
2. **Handle publisher restrictions correctly**: If a publisher restricts a purpose for a vendor, that takes precedence over user consent.
3. **Special Feature opt-ins are NOT purposes**: Special features (precise geolocation, device fingerprinting) require explicit opt-in separately from purposes.
4. **Purpose 1 special handling**: Some jurisdictions treat device storage (Purpose 1) specially under ePrivacy. The `purposeOneTreatment` flag handles this.
5. **Legitimate interest transparency**: Even if using LI, you must disclose it transparently. Users have a right to object.
### Common Integration Mistakes
```typescript
// ❌ WRONG: Checking only consent
if (tcData.vendor.consents[vendorId]) {
// Load vendor
}
// ✅ CORRECT: Check consent OR legitimate interest
function canVendorProcess(
vendorId: number,
purposeId: number,
tcData: TCData,
gvl: GlobalVendorList
): boolean {
const vendor = gvl.vendors.get(vendorId);
if (!vendor) return false;
// Check publisher restrictions first
if (tcData.publisher.restrictions[purposeId]?.[vendorId] === 0) {
return false; // Publisher disallowed
}
// Check if vendor has consent for this purpose
const hasConsent =
tcData.vendor.consents[vendorId] &&
tcData.purpose.consents[purposeId] &&
vendor.purposes.includes(purposeId);
// Check if vendor has LI for this purpose
const hasLI =
tcData.vendor.legitimateInterests[vendorId] &&
tcData.purpose.legitimateInterests[purposeId] &&
vendor.legIntPurposes.includes(purposeId);
// Publisher restriction might force consent-only or LI-only
const restriction = tcData.publisher.restrictions[purposeId]?.[vendorId];
if (restriction === 1) return hasConsent; // Consent required
if (restriction === 2) return hasLI; // LI required
// Default: either consent or LI works
return hasConsent || hasLI;
}
```
## FAQ
### What's the difference between disclosed vendors and allowed vendors?
**Disclosed vendors** are vendors whose presence and data processing you're disclosing to the user, even if they don't have consent yet. This is for transparency. **Allowed vendors** is used in service-specific TC Strings to indicate which vendors are actually allowed to process data for this specific service (e.g., a publisher's website).
### How do I handle vendors not in the Global Vendor List?
Use Google's Additional Consent Mode (AC String) for non-TCF vendors. It's a separate string that works alongside the TC String. Alternatively, obtain direct consent outside TCF for those specific vendors.
### What's the maximum size of a TC String?
TC Strings can be very long with hundreds of vendors. Most are 500-2000 characters. There's no hard maximum, but extremely long strings (>4KB) can cause issues with cookie limits or URL length restrictions. Use range encoding and consider service-specific strings to reduce size.
### Do I need consent for special purposes?
No. Special purposes (like fraud prevention and content delivery) don't require consent under GDPR. However, you must still disclose them transparently.
### How often should I refresh the GVL?
Daily at minimum. New vendors join constantly, and vendor purposes change. Stale GVL data can cause compliance issues if you're not recognizing new vendors or updated purpose claims.
## Production-Ready TCF Integration
TCF 2.2 is complex but necessary for programmatic advertising compliance. The key is understanding that it's not just about collecting consent—it's about creating a standardized signal that flows through the entire ad tech ecosystem, enabling vendors to make real-time compliance decisions.
Focus on correct TC String generation, accurate GVL integration, proper publisher restriction handling, and robust CMP API implementation. Test thoroughly with multiple vendor integrations before going live, and monitor for vendor list updates continuously.
The framework is mature and well-supported by IAB-approved libraries, but understanding the underlying mechanics is essential for debugging, customization, and ensuring your implementation truly meets both legal and technical requirements.
M
Marcus Weber, Compliance Director
Rédacteur chez GetCookies, spécialisé dans la conformité en matière de confidentialité, la gestion du consentement et l'optimisation du marketing numérique.
Articles connexes
Data Redaction & Privacy Governance in GetCAPI
Peace of mind for your DPO. How GetCAPI automatically hashes PII, scrubs URLs, and filters events based on regional data residency rules.
13 min de lecture
GDPR for AI Chatbots: Do Conversations Require Consent?
AI Chatbots (Intercom, Drift) collect PII. When do you need consent? The difference between "Support" (Essential) and "Sales" (Marketing) bots.
10 min de lecture
Children's Data Safety: Navigating the Global Crackdown
From the UK's Age-Appropriate Design Code to California. How to build products that are safe for kids by design and compliant with 2026 regulations.
14 min de lecture
Prêt à simplifier le consentement cookies ?
GetCookies rend la conformité RGPD, CCPA et mondiale sans effort. Commencez aujourd'hui.