Skip to main content

Next.js Advanced

IAB TCF

Prepare the IAB configuration

Use the IAB integration when your application needs the Transparency and Consent Framework. Configure an IAB policy, the correct CMP identity and vendor data for your deployment. A demo CMP ID is not a production configuration.

A CMP ID is assigned to a company when it registers as a CMP with IAB Europe (register.consensu.org/CMP). Every TC String names the CMP that wrote it, so an ID you did not register attributes your users' consent records to someone else. Omitting it leaves the add-on unmounted: no __tcfapi, and no TC String written.

Install @c15t/iab alongside your framework adapter, then mount the IAB components inside an existing consent provider connected to Inth.

Next.js uses the React IAB components. There is no c15t/next/iab JavaScript entry point, so import the components from c15t/react/iab and render this Client Component inside the ConsentRoot from your App Router or Pages Router setup.

The IAB banner and dialog render their own styles by default. Leave ConsentRoot's styles option enabled. The banner's styles render in the server HTML, and the dialog loads its rules when opened.

components/iab-consent.tsx
'use client';

import {
	IABProvider,
	IABConsentBanner,
	IABConsentDialog,
} from 'c15t/react/iab';

export const IABConsent = ({ cmpId }: { cmpId: number }) => (
	<IABProvider cmpId={cmpId}>
		<IABConsentBanner />
		<IABConsentDialog />
	</IABProvider>
);

Supply the CMP ID from your deployment's IAB configuration. Keep a persistent preferences entry point through the surrounding consent provider.

Resolve the policy before rendering

IAB components remain hidden until an applicable policy resolves. Forcing a dialog open does not create a policy or vendor list. Inspect resolution and the Global Vendor List response before changing rendering conditions.

IAB presentation follows presentation.prompt.blocking and presentation.preferences.blocking. A blocking dialog needs focus management, scroll locking and a backdrop together.

Mount IABProvider for every IAB policy

IAB is opt-in. The standard ConsentBanner and ConsentDialog do not handle the IAB model, so when a visitor's policy uses it, the backend sends its vendor list and the app has no IABProvider, they throw an IABUnavailableError (code C15T_IAB_UNAVAILABLE): c15t: this visitor's policy uses IAB TCF, but no <IABProvider> is mounted. The error happens during render, on the server and in the browser, so your error boundary or the framework's error overlay shows it. Render IABProvider from c15t/react/iab for those visitors, or remove the iab model from the policy.

When the dialog loads

IABConsentDialog loads its code on demand, so the first page load carries the banner without the preference center. The code downloads at the first of:

  • The browser's first idle period after the page's load event, while the IAB banner is shown.
  • Hover or focus on the banner's Customize button or partners link.
  • The dialog opening.

A visitor who opens the dialog before the download finishes waits for it. open={true} and showTrigger load it on mount, under a model the dialog's models prop lists. Under another model, showTrigger loads only the trigger. Set preloadDialog: 'intent' in the consent provider options to skip the idle download. Idle loading is also skipped with Save-Data on, on 2g and slow-2g connections, and while offline.

Verify vendor and purpose choices

Test category choices alongside vendor, purpose, legitimate-interest and special feature controls used by your integration. Confirm the TC String and __tcfapi reflect the saved state, and verify the vendor's actual requests. A category-only banner test does not establish that the IAB flow works.

Load an external stylesheet

With Tailwind CSS 3, or when you want to put c15t in a named cascade layer, import c15t/next/styles.css and then c15t/next/iab/styles.css at your global stylesheet entry. The IAB stylesheet holds the IAB rules and uses the base stylesheet's tokens. Set styles: false in ConsentRoot's options so the components use those stylesheets:

styles/globals.css
@import 'c15t/next/styles.css';
@import 'c15t/next/iab/styles.css';

Tailwind CSS 3 also needs the PostCSS plugin.

Restrict how vendors use a purpose

Publisher restrictions override a vendor's declared use of one purpose. Pass them to IABProvider as publisherRestrictions. The vendor IDs below are placeholders; use vendors from your list whose declarations allow each restriction.

components/iab-consent.tsx
'use client';

import {
	IABProvider,
	IABConsentBanner,
	IABConsentDialog,
} from 'c15t/react/iab';

const publisherRestrictions = [
	// Vendors 10 and 755 may not use purpose 4.
	{ purposeId: 4, restrictionType: 0 as const, vendorIds: [10, 755] },
	// Vendor 755 must use legitimate interest for purpose 7.
	{ purposeId: 7, restrictionType: 2 as const, vendorIds: [755] },
];

export const IABConsent = ({ cmpId }: { cmpId: number }) => (
	<IABProvider cmpId={cmpId} publisherRestrictions={publisherRestrictions}>
		<IABConsentBanner />
		<IABConsentDialog />
	</IABProvider>
);

Each restriction type has a requirement that depends on the vendor list:

restrictionTypeMeaningAllowed when the vendor
0Purpose not allowed on any legal basisdeclares the purpose for consent or legitimate interest
1Consent requireddeclares the purpose for legitimate interest and lists it in flexiblePurposes
2Legitimate interest requireddeclares the purpose for consent and lists it in flexiblePurposes

Type 2 is never allowed for purposes 1, 3, 4, 5 and 6, which TCF allows only with consent. TCF allows restrictions only in service-specific TC strings. c15t always writes those, so the deprecated isServiceSpecific option has no effect on restrictions. c15t copies the restrictions when the CMP starts, so changing your array afterwards has no effect.

c15t checks restrictions when the vendor list loads and again before each TC String is encoded. Unsupported restrictions include type 3, which the spec reserves, a vendor or purpose missing from the list, and a vendor with two restriction types for one purpose. They are never dropped. Loading IAB settings, saving and generating a TC String all reject with a PublisherRestrictionError exported by @c15t/iab. On a createIAB handle these are whenReady(), save() and generateTCString(). No TC String is written and no consent is recorded. Retrying whenReady() does not fetch another vendor list. The error lasts until a new vendor list is set up: with an explicit gvl option that never happens, so saving keeps failing even if the kernel later holds a different list. When the CMP follows the kernel's list instead, a replacement list is checked again and saving works once it accepts the restrictions. When a replacement vendor list makes a restriction unsupported, c15t also clears the TC authority confirmed under the previous list, so gated scripts stop until the configuration is fixed. Whenever c15t withdraws the authority it confirmed, it also removes the TC String from the euconsent-v2 cookie and localStorage entry. Because the check reads the current vendor list, a vendor that stops declaring a purpose or its flexibility makes a restriction invalid. Narrow a vendors allowlist and your restrictions together.

Saved TC Strings carry the restrictions in their PubRestrictions section, and __tcfapi('getTCData') reports them as publisher.restrictions, keyed by purpose ID, then vendor ID. A stored TC String whose restrictions differ from the current configuration is not restored. If the visitor's stored choice is otherwise current, the banner opens again, as it does after a policy change, and closes once the visitor saves. IAB gates that depend on the TC String stay denied until then. c15t also removes the superseded TC String from the euconsent-v2 cookie and localStorage entry, so vendors reading standard storage do not pick it up. An unchanged configuration restores the stored TC String without asking. When reading a string written under TCF policy version 2 or 3, c15t accepts type 2 for purposes 3 to 6, which those versions allowed. Strings c15t writes always follow the current policy.

The React, Vue, Svelte and @c15t/browser/iab preference centres list each vendor under the legal basis the restrictions leave it. A vendor whose purpose now requires legitimate interest gets an objection control instead of a consent toggle, and a prohibited purpose no longer lists the vendor. A purpose whose vendors all use legitimate interest has no consent switch, because turning one off would change nothing the vendors rely on; its objection control is the opt-out. Such a purpose does not decide its c15t category: the category follows the purposes in it that the visitor can consent to. Legitimate interest never grants a category on its own. If none of a category's purposes has a consent basis, nothing can consent to them, Accept All included, so the category stays denied. See categories under an IAB policy. Stack switches cover only the purposes some vendor processes on consent. Display-model rows expose this as hasConsentBasis. Custom preference UIs can call applyPublisherRestrictionsToGVL from @c15t/iab/headless, or pass publisherRestrictions to processGVLForDialog, to get the same vendor declarations. The configured restrictions are on the kernel's IAB state as publisherRestrictions.

Consent-gated scripts, network rules and iframes with a vendorId also apply the confirmed restrictions for that vendor:

  • Type 0 blocks a target that declares the purpose in iabPurposes or iabLegIntPurposes.
  • Type 1 makes a purpose declared in iabLegIntPurposes require purpose and vendor consent. Legitimate interest no longer satisfies it.
  • Type 2 makes a purpose declared in iabPurposes require purpose and vendor legitimate interest. Consent no longer satisfies it.

Types 1 and 2 block the target when the vendor list does not mark the purpose as flexible for that vendor. Targets without a vendorId ignore restrictions. A target that uses only legitimate interest once restrictions apply is not blocked by a refused category, since it needs no consent; its legitimate interest signals and the visitor's objection decide. GPC and strict scope still block it, and the category stays refused for scripts that depend on it. Accept All grants the vendor signal a restriction moves a purpose to, including legitimate interest for a vendor that declares none. Legitimate interest a restriction introduces applies until the visitor objects: the preference centres show it as allowed, and saving without touching it encodes it as allowed. introducedLegitimateInterest from @c15t/iab/headless lists those purposes and vendors for custom preference UIs.

Google's Additional Consent (AC) string carries consent for Google ad tech providers (ATPs) that are not on the IAB Global Vendor List. The TC String cannot name them, so without an AC string those providers get no consent signal and Google cannot serve personalised ads through them.

Take the provider IDs from Google's Additional Consent provider list. c15t ignores IDs that Google does not list. When Google removes a provider from the list, usually because it joined the Global Vendor List, it leaves the AC string on the visitor's next page load.

List the providers on your backend

A self-hosted backend lists the providers in its manifest's iab block, next to customVendors. It loads each provider's name and privacy policy the way it loads the vendor list, caches them, and sends them in /init to every visitor whose policy uses IAB TCF. Framework servers and browsers that resolve /init from the manifest load the same providers.

manifest: {
	iab: {
		enabled: true,
		cmpId: 123,
		additionalConsent: {
			// Google ATP IDs: Meta and Snap Inc.
			providers: [89, 2572],
		},
	},
},

The backend's additionalConsent option sets the cache, which defaults to the gvl cache, and enabled: false stops /init from serving the providers. See instance options.

List the providers in the browser

Offline mode has no backend, and some backends do not list providers. In those setups, pass additionalConsent with your other IAB options: the IABProvider props in React, Next.js and TanStack Start, the createIAB options in JavaScript, and the iab option of the Vue, Nuxt, Svelte, Astro and script-tag setups. A list passed here replaces the one the backend sent.

additionalConsent: {
	providers: [89, 2572],
},

What the visitor sees

The Vendors tab of the preference centre gets a Google Ad Tech Providers section. Each provider shows its name, a link to the privacy policy Google lists for it, and a consent switch. Accept All switches every provider on, and Reject All switches them off. The banner's partner count includes them.

The names and links come from https://gvl.inth.app/additional-consent. The backend fetches them when the providers are listed in its manifest. When they are listed in the browser, c15t fetches them as the CMP mounts, in parallel with the vendor list. To serve the list yourself, set endpoint in the manifest's additionalConsent, or url in the browser option. c15t requests ?providerIds=89,2572 and expects JSON in this shape:

{
	"providers": {
		"89": {
			"id": 89,
			"name": "Meta",
			"policyUrl": "https://www.facebook.com/about/privacy/update"
		}
	}
}

If the list does not load, the response carries no providers and c15t writes no AC string. The TC String works as before.

What vendors receive

After the visitor saves, __tcfapi returns the AC string as addtlConsent, next to tcString, from getTCData, getInAppTCData and addEventListener callbacks. It uses version 2 of Google's format, for example 2~89~dv.2572. The part before ~dv. lists the providers the visitor consented to. The part after it lists the providers they were shown but did not consent to. After Reject All, every provider is in the second part: 2~~dv.89.2572.

A provider counts as consented only when its switch is on and the visitor also consented to TCF purposes 1, 3 and 4: storing information on the device, and creating and using profiles for personalised advertising. Google's spec requires consent to device storage and to personalised ads before a provider can be listed as consented. Without those purposes, the provider is listed as shown only.

c15t writes an AC string only together with a TC String, because Google discards an AC string that arrives without one. It stores the string in the addtl_consent cookie and localStorage entry next to euconsent-v2, and removes both when it withdraws the TC String. A returning visitor gets their saved provider choices back. A provider you add later has not been shown to them, so it stays out of their AC string until they save again. A provider you remove and add back keeps the choice the visitor saved for it.

When the visitor's policy does not use IAB TCF, __tcfapi reports gdprApplies: false and returns no tcString or addtlConsent. CMP API v2 allows only cmpId, cmpVersion and tcfPolicyVersion beside it, plus the status fields.

A self-hosted backend records the AC string with each consent, in the addtlConsent column of the consent table beside the TC String. Run the backend's migrations after upgrading to add the column. Consents recorded before then, and consents saved without a TC String, store null.

Send the string with ad requests

Pixels and ad requests that cannot call __tcfapi take the AC string in the addtl_consent URL parameter. Google's ad renderers replace the ${ADDTL_CONSENT} macro, as in &addtl_consent=${ADDTL_CONSENT}. For requests your own code builds, read the string from __tcfapi, add it next to the TC String, and then send the request. This pixel fires once the CMP answers with TCF data:

window.__tcfapi('getTCData', 2, (data, success) => {
	if (!success || !data.gdprApplies) {
		return;
	}
	const url = new URL('https://ads.example.com/pixel');
	url.searchParams.set('gdpr_consent', data.tcString);
	if (data.addtlConsent) {
		url.searchParams.set('addtl_consent', data.addtlConsent);
	}
	new Image().src = url.toString();
});

Ask Google to list your CMP ID

Google accepts AC strings only from a CMP registered with IAB Europe, using its own CMP ID. It lists the CMPs it has certified on its Additional Consent page. Ask Google to list your CMP ID through its CMP intake form before you rely on the AC string in production.

How the preference centre shows features

The stock IAB preference centres in React, Next.js, Vue, Svelte, Astro and the script tag follow TCF 2.4 and Policies v5.0.b. Special purposes stay in their locked section with a lock icon, because the visitor cannot object to them.

Features get their own section after the special purposes. It shows the IAB standard text for features from the Global Vendor List's standardTexts.features. If the list has no standard text, it shows the iab.preferenceCenter.features.description translation. Each feature lists its name, description, illustrations and the vendors that use it. The section has no switch, lock or vendor toggle, because TCF forbids showing features next to a control that cannot be disabled.

The feature section follows the theme's spacing and typography tokens. Its disclosure arrows are decorative and stay out of the accessibility tree.

A custom preference UI gets the same rows from resolveIABDialogDisplayModel in @c15t/iab/headless. essentialRows holds the special purposes only. featureRows holds the features, with locked: false and toggle: 'none', so render them without a control. featuresStandardText is the standard text, or null when you should use the translation.

Each vendor's privacy policy and legitimate interest links come from its urls[] entry in the Global Vendor List. The stock preference centres pick the entry for the language the banner shows, then English. A custom UI gets the same links from resolveIABVendorUrls(vendor, language) in @c15t/iab/headless, or from policyUrl and legitimateInterestUrl on the vendors processGVLForDialog returns when you pass it language. Vendor lists older than GVL v3 have a single policyUrl field, which c15t still reads.

c15t stores TCF choices per site and browser, which the TCF calls service-specific scope. It does not sync TC strings across devices, and every TC string it writes has IsServiceSpecific set to 1.

Do not use /consent/check or your own account data to skip the TCF banner on another device. That turns the choice into multi-device scope, which the TCF requires you to disclose in the first layer, and c15t does not show that disclosure. The isServiceSpecific option is deprecated. c15t ignores it and logs a warning when you pass false.