Skip to main content

JavaScript API

createConsentRuntime

What the runtime owns

createConsentRuntime builds a consent kernel and connects the modules a page needs, such as stored choices, script loading, iframe gating, network blocking, data clearing, IAB, callbacks and the reload after a withdrawal. It renders nothing. Use it when you render your own UI, connect a framework without a c15t adapter, or share one consent state between several parts of a page. @c15t/browser builds one for you; see choose a headless API.

src/consent-runtime.ts
import { hosted } from 'c15t';
import { createConsentRuntime } from 'c15t/runtime';

import { scripts } from './scripts';

// One runtime for the page: policy, stored choices, script loading,
// iframe blocking and the reload after a revocation.
export const runtime = createConsentRuntime({
	mode: hosted({ backendURL: 'https://your-project.inth.app' }),
	scripts,
});

Creating the runtime has no side effects. It reads no storage and sends no request until start(), so the module can also load on a server.

Options

Only mode is required.

OptionTypeDefaultWhat it does
modetransport factoryrequiredWhere the policy comes from and where choices go: hosted(), offline(), manifest() or custom(). offline() switches copy when the kernel's language changes. See transports. Read once.
scriptsScript[]noneVendor scripts the loader mounts as categories are allowed. See script loader.
consentCategoriescategory namesinferredCategories to offer alongside those your scripts, rules and vendors use, within the policy's scope.
networkBlocker{ rules, enabled?, logBlockedRequests?, onRequestBlocked? } or falseoffHold matching fetch and XHR requests until their category is allowed. See network blocker.
iframeBlocker{ disableAutomaticBlocking? } or falseonGate <iframe data-src data-category>. See iframe blocker.
clearOnRevocationcookies and storage keys per categoryoffDelete named browser data when its category is denied. Read once. See clear on revocation.
persistenceboolean or { storageConfig?, skipHydration?, sync? }trueRead and write choices in the cookie and localStorage, and follow other tabs. false keeps choices in memory only. See persistence.
storageConfig{ storageKey?, crossSubdomain?, defaultDomain?, defaultExpiryDays? }key c15t, 365 daysCookie and localStorage naming and lifetime.
reloadOnConsentRevokedbooleantrueReload after an accept, reject or save turns off a category or vendor that was allowed, once the save request finishes.
callbacks{ onChoiceRecorded?, onPermissionsChanged?, onError?, onBeforeConsentRevocationReload? }noneSee callbacks.
overrides{ country?, region?, language?, gpc? }noneDecision inputs the page knows.
prefetchkernel configurationnoneA server-resolved init answer. With a resolved policy, start() skips the /init request. A prefetch marked initialPolicyPending is provisional, so start() still sends it.
policyRulesPolicyRule[]noneRules a local transport resolves.
i18n{ locale?, messages }EnglishInitial language, and messages that override the bundled or backend copy key by key for the same language. See translations.
presentation{ prompt?, preferences? }noneLayout and blocking, for UIs that call resolveConsentPresentation.
journey'page', 'tab' or false'page'Random id that links each /init to the save that follows. See session reports.
user{ externalId, identityProvider? }noneAn identified visitor.
vendorsVendor[]noneVendors offered for vendor-level consent outside IAB.
noncestringnoneCSP nonce the script loader puts on every <script> it creates. A script's own nonce wins.
scriptLoader{ onDebug? }noneReceive every script loader lifecycle event.
iabIAB options or falsenoneCMP settings. Needs createIAB.
createIABcreateIAB from @c15t/iabnoneThe IAB module factory. Without it, iab is ignored.
enabledbooleantruefalse grants every category, skips init and mounts no blocker, persistence or IAB. Scripts load at once.
windowDebugbooleantrueInstall a small window.c15t object with version, pkg, mode and hosting on start(). hosting reads the current snapshot, so it changes from null once /init reports it.
pkgstring'@c15t/core'The package name window.c15t reports.

What start and dispose do

runtime.start() mounts, in order:

  1. the window.c15t debug object, unless windowDebug: false;
  2. persistence, which reads stored choices;
  3. the /init request. With a resolved prefetch it adopts that answer instead: the choice is evaluated at the server's clock, the banner the server rendered counts as the first impression, a detected Global Privacy Control signal is honoured, and init:applied fires as it would for a response;
  4. the script loader, when scripts is not empty;
  5. the network blocker, when configured;
  6. the iframe blocker, unless iframeBlocker: false;
  7. the IAB module, when iab and createIAB are set;
  8. data clearing, when clearOnRevocation is set.

start() does nothing without a document, so you can call it from shared code. A second call does nothing. The callback bridge and the reload watcher attach at construction, so they hear events from the first start().

In a client-only app, with no server-rendered markup to hydrate, you can call start() before the first render: the /init request then runs while the app mounts, and nothing waits for it. With server-rendered markup, call it after hydration so the first browser render matches the server's.

The network blocker and data clearing are opt-in, so they load as separate chunks, and only when configured. Network blocker rules hold matching requests from construction until the blocker has loaded and decides each one. Data clearing waits for the policy anyway.

runtime.dispose() undoes everything in reverse, then disposes the kernel. A runtime disposed before it started blocks the requests it was holding.

Runtime handle

MemberWhat it does
kernelThe kernel. Read and change consent through it.
start(), dispose()Mount and unmount every browser side effect.
startedWhether start() has run and the runtime is not disposed.
identify(user)Link the consent record to a user. Failures surface through onError, not a rejection.
setOverrides(overrides)Merge into the country, region, language and GPC inputs. A key you pass replaces its value, undefined clears it, and a key you leave out stays.
reinit()Run /init again, for example after setOverrides. Does nothing while disabled.
setLanguage(code)Show the copy in another language. Does nothing for the current language; otherwise runs /init again, unless the runtime is disabled or uses consentSource.
processIframes()Pause gated iframes that consent does not allow and restore the ones it allows. Needed only with iframeBlocker: { disableAutomaticBlocking: true }. Does nothing before start(), after dispose() or with iframeBlocker: false.
reconcileStorage()Read stored records again and apply changes another runtime or tab made. Returns whether anything changed.
clearRecords()Delete stored choices and reset the kernel's records: choice, notice dismissal, subject and vendor choice. Saves still queued for the cleared subject are dropped.
consentCategoriesnecessary plus the categories in the current choice scope, in the order the preference dialog lists them.
setConsentCategories(categories)Replace the configured categories. Discovered ones stay. undefined drops the configured list.
experimentThe experiment the runtime runs: the one a ready prefetch carries, otherwise the experiment option. Resolve presentation and theme against it.
iabThe CMP handle under IAB, or null.
subscribe(listener)Called when iab is mounted, replaced or removed; read it again in the listener. Returns an unsubscribe function. A provider runtime also calls it when kernel or enabled changes.

Runtime for a framework provider

createConsentRuntime is for a page that configures consent once. A framework provider whose options follow its props uses createConsentProviderRuntime(options, modules) instead. It has every member above, plus:

MemberWhat it does
update(options)Apply the provider's new options. Pass the whole option set each time; the runtime compares it with the previous one and applies only what changed. Returns a promise that settles once every change has applied.
setEnabled(enabled)Turn consent management off or on. Off renders a separate permissive kernel that grants every category and shows no UI; only the script loader runs, and no request waits for the network blocker. On renders the visitor's kernel again, with their records, and adopts the prefetch again. It runs /init instead when there is no prefetch, or when overrides or the language changed while it was off.
enabledWhether consent management is on.
kernelThe kernel to render. It changes when enabled changes.
subscribe(listener)Called when kernel, iab or enabled changes. Read them again in the listener. Returns an unsubscribe function.

update() applies these options to a running runtime:

OptionOn change
enabledSame as setEnabled().
userThe new user is identified. The user the runtime was created with is sent with /init and every save, and is not identified separately.
overridesThe overrides are set and /init runs again. Key order does not count as a change. Before start() or while disabled, the next start sends /init instead of adopting the prefetch.
consentCategoriesThe configured categories are replaced.
vendors, scripts, network blocker rulesVendors are declared again. Scripts go to the script loader, rules to the network blocker. A loader starts when scripts first appear.
networkBlockerNew rules and enabled apply; leaving enabled out turns the blocker on. Requests new rules match wait until the blocker has them, including while an on-demand blocker loads. false removes the blocker; options where there were none add one.
iframeBlockerfalse removes the blocker. A new disableAutomaticBlocking rebuilds it.
callbacks, reloadOnConsentRevokedRead when an event fires, so the latest values apply.

enabled, overrides and consentCategories apply before update() returns. The rest of the comparison loads with the first update() in which some option is a new value. Calling update() with the option values the runtime already has, as a mount-time effect does, downloads nothing. Those changes apply once it has loaded, and the returned promise settles then. Until the network blocker has new or wider rules, requests they match are held, the way the runtime holds requests from construction until the blocker loads, so none goes out unchecked.

nonce, scriptLoader.onDebug and the network blocker's logBlockedRequests and onRequestBlocked are read when their module starts. Every other option is read once. Storage counts among them: persistence keeps reading and writing where it started, and data clearing keeps protecting that location. Outside production, a change to mode, i18n, experiment, persistence or storageConfig logs a warning; create a new runtime to change them.

prefetch may also be a promise, for example the result of a server helper that a framework streams to the browser, when modules includes streamPrefetch from c15t/runtime, or lazyStreamPrefetch from c15t/runtime/provider, which loads that code only for a runtime whose prefetch is a promise. Without either, a pending prefetch is ignored with a warning outside production, and the runtime requests the policy itself. That keeps the code out of providers that never stream. The runtime starts with a provisional policy, so no consent surface shows, and its first /init waits for the promise and applies the result instead of sending a request. A result without a policy is applied as a baseline (records, location, language) and the request is sent with those overrides. A rejected promise sends the request as if there were no prefetch. Records cleared while the promise is pending stay cleared. An experiment in a streamed result arrives too late to run.

modules decides how each module loads. Pass defaultRuntimeModules to load them the way createConsentRuntime does. To keep a module out of the first chunk, replace its factory with lazyRuntimeModule(() => import(...)): calls made before the module has loaded are queued and replayed. A module that fails to load stays inactive and, outside production, logs a warning. Keep watchRevocationReload static, because it has to see the first save, and prefer a static createPersistence, because a lazy one shows a returning visitor the banner until it has loaded. When the script loader is lazy, data clearing subscribes once it has loaded, so revocation callbacks still run before browser data is removed. IAB mounts through the mountIAB module (mountRuntimeIAB, included in defaultRuntimeModules); without it iab is ignored.

onDemandRuntimeModules loads the script loader, the network blocker, data clearing and a consentSource connection on demand, in chunks that import nothing your first chunk has. The script loader and the network blocker share one chunk, so a page with scripts and blocker rules downloads, or preloads, one file. Spread it into modules next to the modules you import statically:

import { watchRevocationReload } from 'c15t';
import { createIframeBlocker } from 'c15t/modules/iframe-blocker';
import { createPersistence } from 'c15t/modules/persistence';
import { createWindowDebug } from 'c15t/modules/window-debug';
import { onDemandRuntimeModules } from 'c15t/runtime/on-demand';
import { createConsentProviderRuntime } from 'c15t/runtime/provider';

const runtime = createConsentProviderRuntime(options, {
	...onDemandRuntimeModules,
	createIframeBlocker,
	createPersistence,
	createWindowDebug,
	watchRevocationReload,
});

A page that configures none of those modules never downloads them. A page with only scripts, or only blocker rules, downloads both. Consented scripts mount, and matching requests stay held, until the shared chunk has loaded. Optional categories stay denied until a consentSource connects. A module whose chunk fails to load is tried again when the browser comes back online. Until then a failed network blocker blocks every request its rules match instead of holding it. A module you load yourself with lazyRuntimeModule works the same way, but if it imports code your first chunk also uses, bundlers that split shared code (Vite, esbuild) move that code into extra chunks the first load then fetches.

Import from c15t/runtime/provider when your provider loads modules on demand. It exports createConsentProviderRuntime, lazyRuntimeModule and lazyStreamPrefetch, and none of the modules defaultRuntimeModules imports statically. Some bundlers, esbuild among them, keep a module in the first chunk when it is imported statically anywhere in the graph, even unused, and also through import().

c15t/runtime/on-demand exports onDemandRuntimeModules, createConsentRuntimeWith and mountRuntimeIAB. createConsentRuntimeWith(options, modules) is createConsentRuntime with the modules you choose, for a page that configures once (a script tag, an Astro page) and loads some modules on demand. The same lifecycle and handle apply. The two entries are separate because esbuild emits a chunk for every import() in the files it reaches, used or not. A provider that loads modules through its own import() calls would get the on-demand chunks as well, plus an extra chunk for each module both load, and a page that configures once would get the provider runtime's update and streamed-prefetch chunks.

c15t/runtime/on-demand-factories exports each on-demand factory on its own: scriptLoaderOnDemand, networkBlockerOnDemand, clearOnRevocationOnDemand and connectConsentSourceOnDemand. scriptLoaderOnDemand and networkBlockerOnDemand each load a chunk with only their module. Use them when you import some of those modules statically and load the rest on demand. Spreading onDemandRuntimeModules instead would load the module you import statically a second time, in the shared chunk, and a bundler then moves that module into a chunk of its own. The factories have their own entry because esbuild emits a chunk for every import() in the files it reaches, used or not, which would split the shared chunk apart:

import { watchRevocationReload } from 'c15t';
import { createIframeBlocker } from 'c15t/modules/iframe-blocker';
import { createPersistence } from 'c15t/modules/persistence';
import { createScriptLoader } from 'c15t/modules/script-loader';
import { createWindowDebug } from 'c15t/modules/window-debug';
import { createConsentRuntimeWith } from 'c15t/runtime/on-demand';
import {
	clearOnRevocationOnDemand,
	connectConsentSourceOnDemand,
	networkBlockerOnDemand,
} from 'c15t/runtime/on-demand-factories';

const runtime = createConsentRuntimeWith(options, {
	connectConsentSource: connectConsentSourceOnDemand,
	createClearOnRevocation: clearOnRevocationOnDemand,
	createIframeBlocker,
	createNetworkBlocker: networkBlockerOnDemand,
	createPersistence,
	createScriptLoader,
	createWindowDebug,
	watchRevocationReload,
});
runtime.start();

Other exports

Export from c15t/runtimeWhat it does
createLazyIABFactory(loader)Wrap a dynamic import('@c15t/iab') so the IAB code loads as a separate chunk when the runtime mounts IAB. Pass its create as createIAB.
isIABConfigured(iab)Whether an iab option turns IAB on.
wireRuntimeCallbacks({ kernel, callbacks })Attach the four callbacks to a kernel you own. Returns a disposer.
createConsentProviderRuntime(options, modules)The runtime for a framework provider. See runtime for a framework provider.
defaultRuntimeModulesThe module factories createConsentRuntime mounts.
lazyRuntimeModule(load)Wrap a module factory so its module loads through a dynamic import() on first use.
streamPrefetchAdd to the provider runtime's modules to accept a prefetch promise.
mountRuntimeIABThe mountIAB module: mounts the CMP once the kernel knows its cmpId.

Check it works

  1. Create the runtime and subscribe to runtime.kernel before start().
  2. Call start() in the browser. The Network tab shows one /init request, and your subscriber receives a snapshot whose resolution.status is matched.
  3. Call runtime.kernel.commands.save('none') from a button. A /subjects request follows and reloading keeps the choice.
  4. Call runtime.dispose(). Scripts the loader added are removed.