Skip to main content

JavaScript API

Kernel API

Get a kernel

The kernel holds consent state and runs commands. Every JavaScript setup has one:

  • consent.kernel, from init() in @c15t/browser.
  • runtime.kernel, from createConsentRuntime in c15t/runtime.
  • A kernel you create yourself, as shown below.

The kernel alone supplies state and commands. It renders nothing, loads no script and reads no storage; the modules around it do that.

const unsubscribe = kernel.subscribe((snapshot) => {
	console.log(snapshot.resolution.status, snapshot.effectivePermissions);
});

This fragment assumes one of the kernels above. Keep unsubscribe and call it during teardown.

Read state

MemberReturnsWhat it does
getSnapshot()ConsentSnapshotThe current state, frozen. Cheap to call. See the snapshot reference.
subscribe(listener)() => voidCall listener with each new snapshot.
getServerSnapshot()ConsentSnapshotThe first snapshot, as a server render saw it. Render from this during hydration, then switch to getSnapshot().
refresh(now?)ConsentSnapshotEvaluate again at now, so an expired choice cannot linger behind a timer. Returns the new snapshot.

A snapshot keeps the same object for a field whose value did not change, so previous.effectivePermissions === next.effectivePermissions tells you permissions did not change.

Choose commands by intent

Commands are asynchronous and are the only way to record anything.

CommandReturnsEffect
commands.init()Promise<{ ok, error? }>Resolve the policy through the transport. Failed attempts retry in the background.
commands.save('all')Promise<SaveResult>Record acceptance of every category in scope.
commands.save('none')Promise<SaveResult>Record rejection of every optional category in scope.
commands.save({ marketing: false })Promise<SaveResult>Record only the categories you name.
commands.save()Promise<SaveResult>Record the staged draft, falling back to the current choice or the policy's defaults for each category.
commands.dismissNotice()Promise<NoticeDismissResult>Record that the visitor saw a notice. Grants nothing. Only works while promptRequirement.kind is notice.
commands.identify(user)Promise<void>Link the record to { externalId, identityProvider?, identityToken? } and load the server's record for that subject. The token verifies the link.

save(input, context) takes a second argument. context.categories narrows 'all', 'none' or an empty input to the categories your UI displays, which is how @c15t/browser limits Accept All to the dialog's categories. context.vendors records vendor-level grants.

A save updates permissions and notifies subscribers before the backend request starts. SaveResult.ok reports the request. When it fails, the choice stays in effect and the payload is queued for retry. Consent state documents the ordering.

Change inputs and UI state

set operations run synchronously and notify subscribers. None of them records a choice.

SetterWhat it changes
set.activeUI('banner' | 'dialog' | 'none')The surface your UI should show.
set.draft(values)Staged category values that a no-argument save() confirms. Never a grant.
set.overrides({ country, region, language, gpc })Decision inputs, merged into the current ones; undefined clears a key. Call commands.init() after it to resolve the policy again.
set.language(code)The language override.
set.privacySignals({ gpc })The detected GPC signal. c15t reads the browser's signal itself; use this to pass one detected elsewhere, such as a Sec-GPC request header.
set.consentCategories(categories)The configured categories. Discovered categories stay.
set.registerConsentCategories(categories)Add categories your integrations use. They stay for the kernel's lifetime.
set.vendors(patch, options?)Merge declared vendors.
set.subjectId(id)The subject ID.
set.iab(patch)Written by the IAB module. Do not call it yourself.

Apply stored records

Pass validated records as initialRecords to createConsentKernel(), such as a choice read from a cookie with readStoredRecordsFromCookieHeader(). They apply without counting as a visitor action. In the browser, createPersistence() reads storage into the kernel on mount and keeps tabs in step afterwards.

Events

kernel.events.on(type, listener) returns an unsubscribe function. Each listener receives the event object.

EventFieldsWhen
init:appliedsnapshotA policy resolved, from the transport or a resolved prefetch.
init:failederror, attempt, nextRetryMsInitialization failed. nextRetryMs is null when no retry remains.
choice:recordedsnapshot, confirmed, actionAtAn accept, reject or save recorded categories.
permissions:changedsnapshot, previousEffective permissions changed value, for any reason.
notice:dismissedsnapshot, dismissalA notice was acknowledged.
vendors:recordedsnapshot, actionAtA save changed which vendors are denied.
vendors:setsnapshotThe declared vendor list changed.
overrides:setsnapshotDecision inputs changed.
user:identifiedsnapshotidentify linked a user.
subject:resolvedsnapshotA save received the server's subject ID.
iab:setsnapshotThe IAB state changed.
records:clearednoneStored records were cleared.
save:replayedsubjectId, ok, rejected?A queued save was sent again. rejected holds the backend's refusal code.
command:init:started, command:save:startednoneA command began.
command:init:completed, command:save:completedresultA command finished.
command:errorcommand, errorA command failed.

Use choice:recorded to react to a visitor's decision and permissions:changed to start and stop code. Callbacks explains the difference.

How listeners are notified

kernel.subscribe() and kernel.events.on() listeners run synchronously when a change commits, in registration order. Each receives the frozen snapshot that change produced.

  • A listener that throws does not stop the listeners after it or fail the command that caused the change. In a browser page c15t passes the error to reportError. Everywhere else, including Bun and Deno servers where reportError would end the process, it logs the error with console.error.
  • A listener can call commands and setters. Before the nested call returns, every other listener receives the change in progress and then the new one. No listener misses a change, such as a denial that another listener immediately reverses, or receives changes out of order.
  • A listener added during a notification first receives the next change. A listener removed during a notification is not called again.
  • Listeners that keep changing consent in response to each other are stopped after 100 nested notifications, and the error is reported. Notifications already queued still arrive, so other listeners end on the current snapshot. A listener that changes consent after that point is not called again.

Create a kernel yourself

Most apps should use createConsentRuntime or @c15t/browser, which connect the modules for you. Create the kernel directly only when you need to choose every module:

src/consent.ts
import { createConsentKernel, createHostedTransport } from 'c15t';
import { createPersistence } from 'c15t/modules/persistence';
import { createScriptLoader } from 'c15t/modules/script-loader';

import { scripts } from './scripts';

export const startConsentKernel = async function startConsentKernel(
	backendURL: string
) {
	const kernel = createConsentKernel({
		transport: createHostedTransport({ backendURL }),
	});
	// Each module is yours to create and dispose. This kernel has no iframe
	// blocker, network blocker, data clearing or reload after revocation.
	const persistence = createPersistence({ kernel });
	const loader = createScriptLoader({ kernel, scripts });
	await kernel.commands.init();
	return {
		dispose() {
			loader.dispose();
			persistence.dispose();
			kernel.dispose();
		},
		kernel,
	};
};

This kernel stores choices and loads scripts. It has no iframe blocker, network blocker or data clearing, and it does not reload the page when a visitor withdraws permission, so a vendor that already ran keeps running. Add each one you need from its c15t/modules/* entry point, and dispose it before the kernel:

ModuleEntry pointPage
Persistencec15t/modules/persistencePersistence
Script loaderc15t/modules/script-loaderScript loader
Network blockerc15t/modules/network-blockerNetwork blocker
Iframe blockerc15t/modules/iframe-blockerIframe blocker
Clear on revocationc15t/modules/clear-on-revocationClear on revocation
Reload after withdrawalwatchRevocationReload from c15tThis page, below

watchRevocationReload({ kernel, isEnabled, getOnBeforeReload? }) reloads the page after a save turns off something that was allowed. It returns a disposer.

createConsentKernel(config) takes a transport, not a transport factory. Use createHostedTransport({ backendURL }) or createOfflineTransport() from c15t, or your own object with init and save. Other useful config fields:

FieldWhat it does
transportCarries init, save, identify and loadSubjectRecord. Without it, commands succeed and send nothing.
consentCategoriesCategories to offer, within the policy's scope.
initialOverrides, initialUserDecision inputs and an identified user at start.
initialRecords, initialPolicyResolutionRecords and a policy a server already resolved.
initialTranslationsCopy to start with.
translationOverridesMessages keyed by language, such as your i18n.messages. The kernel applies the entry for the active language over initialTranslations and over each init response, key by key.
initRetryRetry for a failed init: { maxAttempts, baseDelayMs, maxDelayMs }, or false. Defaults to 5 attempts from 1 second up to 30 seconds.
nowThe evaluation clock at start, for a server render and its hydration to agree.

Construction has no side effects, so a server can build a kernel and read its snapshot.

Check it works

  1. Subscribe to the kernel and log policyPending and resolution.status. After commands.init(), policyPending is false and the status is matched.
  2. Add an events.on('choice:recorded', …) listener. It logs once for each accept, reject or save, and not on reload.
  3. Call kernel.dispose(). Background retries stop; getSnapshot() still works.