Zurück zum Blog
Compliance

Understanding IAB TCF 2.2 Framework: Complete Technical Guide

Marcus Weber, Compliance DirectorOctober 15, 202425 Min. Lesezeit
IAB TCFConsent FrameworkGDPRAdTech
Understanding IAB TCF 2.2 Framework: Complete Technical Guide
--- 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. ---

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.

Read full summary The IAB Transparency and Consent Framework 2.2 provides a standardized technical specification for obtaining, storing, and communicating user consent across the digital advertising ecosystem. This guide covers TCF architecture, TC String encoding/decoding, vendor list integration, purpose and feature management, Global Vendor List (GVL) usage, publisher restrictions, and implementation patterns. Includes production TypeScript examples for building TCF-compliant CMPs, integrating with ad tech stacks, and handling complex consent scenarios. *Summary by Claude AI*
## 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

Autor bei GetCookies, spezialisiert auf Datenschutz-Compliance, Einwilligungsmanagement und Optimierung von digitalem Marketing.

Bereit, Cookie-Einwilligung zu vereinfachen?

GetCookies macht DSGVO, CCPA und globale Datenschutz-Compliance mühelos. Starten Sie heute.