Skip to main content

Astro Advanced

IAB TCF

Before you start

Use IAB TCF when your site works with advertising vendors that read a TC String. You need:

  • An Inth project whose policy for the relevant regions uses the iab model.
  • A CMP ID. IAB Europe assigns one when a company registers as a CMP at register.consensu.org/CMP. Every TC String names the CMP that wrote it, so use the ID your company registered.

The c15t package includes the IAB code, so there is nothing else to install.

Turn on IAB in the integration

Add iab to the integration options from the quickstart:

astro.config.mjs (partial)
c15t({ iab: { cmpId: 123 } });

Replace 123 with your registered CMP ID. A hosted backend can supply the CMP ID through /init, so cmpId is optional when your Inth project has one. The integration inlines the IAB banner's rules once per page. Its preference center rules load before the IAB dialog opens, so there is no IAB stylesheet request before the page's first paint. Tailwind CSS 3 sites use the full external IAB stylesheet so the host's CSS pipeline can process it.

IAB stays off until you set iab. Without it, or with iab: false or enabled: false, the page script contains no CMP code and the browser never downloads @c15t/iab.

If a visitor's policy uses the iab model and the backend sends its vendor list while iab is unset, the server render throws an IABUnavailableError (code C15T_IAB_UNAVAILABLE). A page the browser resolves itself, such as a prerendered one, throws it there as an uncaught error. The standard banner does not handle the IAB model, so the visitor would otherwise get no working consent UI. Set iab, or remove the iab model from the policy. A backend that answers gvl: null turns IAB off for the request, and the policy runs as opt-in.

OptionEffect
cmpIdYour registered CMP ID
cmpVersionThe CMP version __tcfapi reports
vendorsLimits the vendor list to these vendor IDs
publisherCountryCodePublisher country written into the TC String
publisherRestrictionsRestrictions written into the TC String and applied to vendors
gvlA vendor list to use as-is, for offline mode
gvlURLWhere the server fetches the vendor list, for offline mode
enabledSet false to keep the configuration but turn IAB off

Render the IAB banner and dialog

Replace the standard banner and dialog with the IAB pair. The footer trigger opens the IAB preference center:

src/layouts/iab.astro
---
import { ClientRouter } from 'astro:transitions';
import {
	ConsentDialogLink,
	ConsentScript,
	IABConsentBanner,
	IABConsentDialog,
} from 'c15t/astro/components';

interface Props {
	title: string;
}

const { title } = Astro.props;
---

<html lang="en">
	<head>
		<meta charset="utf-8" />
		<meta content="width=device-width, initial-scale=1" name="viewport" />
		<title>{title}</title>
		<ConsentScript />
		<ClientRouter />
	</head>
	<body>
		<slot />
		<footer>
			<ConsentDialogLink kind="iab">Privacy settings</ConsentDialogLink>
		</footer>
		<IABConsentBanner />
		<IABConsentDialog />
	</body>
</html>

IABConsentBanner lists the purposes and the number of partners from the vendor list, and renders nothing until a vendor list is available. Its partners link opens IABConsentDialog on the vendors tab. See IABConsentBanner for props.

With server output, the layout can choose per request. When only some visitors get an IAB policy, render the IAB pair when Astro.locals.c15t.snapshot.model is 'iab' and the standard pair otherwise.

Where the vendor list comes from

ModeVendor list source
manifest()The server resolves it, and the browser reads it through the same-origin /api/c15t/init route
hosted()The browser fetches it from the backend's /init after the page loads
offline()The gvl or gvlURL option

In hosted mode, the backend's /init must be reachable from the browser and accept requests from your site's origin without server-only credentials. For a private backend, put a public same-origin proxy in front of it that adds the credentials on the server.

On a static page, the build has no vendor list, so the IAB banner appears once the browser has one.

Publisher restrictions

Pass publisherRestrictions in the iab options. Each entry names a purposeId, a restrictionType and the vendorIds it applies to.

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.

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.

Check the IAB setup

Open the site in a private window from a region your IAB policy covers:

  1. The IAB banner shows the purposes and a partner count. No advertising vendor requests appear in DevTools Network.
  2. Select the partners link. The preference center opens on the vendors tab.
  3. Save a choice. In the console, __tcfapi('getTCData', 2, console.log) reports a TC String, and an euconsent-v2 cookie exists.
  4. Reload. The banner stays closed and the TC String is the same.
  5. Open Privacy settings from the footer. The IAB preference center opens with your saved choices.

Test the vendors' own requests as well. A category-only test does not show that the IAB flow works.