JavaScript Advanced
IAB TCF
Use the IAB build of @c15t/browser
With the stock UI from the quickstart,
import init from @c15t/browser/iab instead of @c15t/browser. This entry
adds the CMP, the TC string encoder, the IAB banner and the purpose and vendor
dialog. The plain entry leaves them out. Its stock banner stands aside for a
policy that uses the iab model, so a client that resolves one emits an
error event and throws an IABUnavailableError (code
C15T_IAB_UNAVAILABLE) as an uncaught error. So does the IAB entry with
iab: false.
Replace https://your-project.inth.app with your project's backend URL and
YOUR_CMP_ID with your registered CMP ID, as explained below. The
second half shows confirming individual choices from code; the stock dialog
does this for visitors. ready() waits for the policy, and whenReady() waits
for the vendor list and CMP setup. acceptAll() and rejectAll() confirm
through the CMP under an IAB policy. save({ ... }) with categories fails
under an IAB policy because it cannot produce a TC string; use saveIAB().
When the resolved policy is not an IAB policy, this entry shows the ordinary
banner. For your own stylesheet with ui: { shadow: false }, import both
@c15t/browser/styles.css and @c15t/browser/iab/styles.css.
Attach the module to your kernel
Without @c15t/browser, install @c15t/iab with c15t. The following
fragment assumes an existing kernel connected to Inth and an applicable IAB
policy:
Supply your deployment's CMP ID and vendor configuration. Do not copy a demo ID
into production: IAB Europe assigns CMP IDs at registration
(register.consensu.org/CMP), every TC
String names the CMP that wrote it, and createIAB throws unless the ID is an
integer from 2 to 4095 rather than encoding a string that names no registered
CMP. The module installs the CMP API, manages vendor data and can encode the TC
String. It does not render your banner or preferences UI.
To expose the same TC String through the Global Privacy Platform API
(__gpp), and US state opt-outs alongside it, add
IAB GPP.
Restrict how vendors use a purpose
Publisher restrictions override a vendor's declared use of one purpose. Pass
them as publisherRestrictions to createIAB, in the runtime's iab
options, or in the iab options of the Astro integration. The vendor IDs
below are placeholders; use vendors from your list whose declarations allow
each restriction.
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.
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
0blocks a target that declares the purpose iniabPurposesoriabLegIntPurposes. - Type
1makes a purpose declared iniabLegIntPurposesrequire purpose and vendor consent. Legitimate interest no longer satisfies it. - Type
2makes a purpose declared iniabPurposesrequire 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.
Own the lifecycle
Dispose the IAB handle before disposing its kernel. When using a shared runtime, provide the IAB factory through that runtime's configuration instead of creating a second module for each UI root. Await lazy readiness before calling a lazily loaded handle's actions.
A complete implementation needs policy-aware actions, vendor and purpose
controls, persistent preferences access and keyboard interaction. Test the
resulting TC String, __tcfapi and actual vendor requests. Keep backend, schema,
core and IAB packages on compatible v3 versions.
Server-rendered vendor data
Server integrations serialize a gvlReference and the banner's summary instead
of embedding the full Global Vendor List. The reference contains its URL,
language and vendorListVersion. The banner shows the same purpose names and
vendor count on the server and during hydration.
Configure vendor filtering on the server when the banner must render immediately.
An additional client-only vendors allowlist makes the server summary unsuitable:
the banner waits for the full list, then shows the filtered count and disclosures.
Unknown or duplicate IDs do not increase the count. This case adds the list fetch
to banner visibility time; the default server-configured path does not wait.
The IAB module fetches the full list when it mounts in the browser. Loading starts before interaction so CMP initialization and validation of returning visitors' consent do not wait for a click. Early Accept and Reject actions wait for the list before encoding a TC String. A failed fetch leaves the banner visible and does not grant consent. A later action retries a failed request. A list version mismatch requires a new init response or a reload. A new reference from initialization or a language change starts a new load and cancels actions queued for the previous list. Existing TCF listeners are retained. Client-only manifest transports keep their fetched GVL, including custom fetcher filtering; deferral applies at server-rendering boundaries.
Astro, TanStack Start, Nuxt, Next.js and SvelteKit manifest init handlers also serve
versioned list requests on their existing init route. These responses contain
only the public list and use public, max-age=86400. Resolved init data remains
private. Next.js and Svelte hosted prefetch retain the inline list when cookies,
forwarded headers or a custom fetch are required. Those server credentials are
not serialized into a browser reference. The list URL includes its language and version; a route returns an
uncacheable 409 if it can no longer supply that version. Responses are not
marked immutable because translations or publisher filtering can change
without a new vendor-list version.
Astro, Next.js and TanStack Start use a same-origin route when routePrefix
names a mounted consent route. Astro mounts one at /api/c15t by
default. Without those options, Next.js and TanStack Start reference the public
list URL from the manifest. Allow 'self' in CSP connect-src for same-origin
routes, or allow the public list origin for direct requests.
Hosted init responses use an init reference to read the list from
the existing init endpoint. These references include the original country,
region and GPC inputs so a hosted request resolves the same regional policy.
They do not carry credentials. The inputs travel as query parameters, so a
cross-origin request needs no CORS preflight.
The endpoint keeps its existing private caching
rules. Allow the referenced origin in your CSP connect-src directive.
An explicit gvl supplied by your application still takes precedence, including
gvl: null to disable IAB. Applications that supply a list inline are responsible
for its serialized size. Vendor allowlists still apply after the list loads.