---
title: IAB TCF
description: Render the IAB TCF 2.4 banner and preference center on an Astro
  site with the c15t integration, and configure the CMP ID, vendor list source
  and publisher restrictions.
group: frameworks
lastModified: "2026-10-10T16:01:45+01:00"
---
## 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](https://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](/docs/frameworks/astro/quickstart):

```js title="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.

|Option|Effect|
|--|--|
|`cmpId`|Your registered CMP ID|
|`cmpVersion`|The CMP version `__tcfapi` reports|
|`vendors`|Limits the vendor list to these vendor IDs|
|`publisherCountryCode`|Publisher country written into the TC String|
|`publisherRestrictions`|Restrictions written into the TC String and applied to vendors|
|`gvl`|A vendor list to use as-is, for offline mode|
|`gvlURL`|Where the server fetches the vendor list, for offline mode|
|`enabled`|Set `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:

```astro title="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](/docs/frameworks/astro/components/iab-consent-banner) 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

|Mode|Vendor 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:

|`restrictionType`|Meaning|Allowed when the vendor|
|--|--|--|
|`0`|Purpose not allowed on any legal basis|declares the purpose for consent or legitimate interest|
|`1`|Consent required|declares the purpose for legitimate interest and lists it in `flexiblePurposes`|
|`2`|Legitimate interest required|declares 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](/docs/concepts/consent-state#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.

## Consent scope

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.
