---
title: "@c15t/browser API"
description: Reference for the @c15t/browser ES module in a bundled JavaScript
  app, covering its entry points, init and createConsentClient, the client's
  methods and properties, events, page hooks and helper exports.
group: frameworks
lastModified: "2026-10-10T16:01:45+01:00"
---
## Entry points

|Import|What it gives you|
|--|--|
|`@c15t/browser`|`init`, `createConsentClient` and `mountConsentUI`, with the stock banner, dialog and trigger. `mode` is required: a factory from `manifest()`, `hosted()`, `offline()` or `custom()`.|
|`@c15t/browser/hosted`|The stock UI and client for hosted mode only. Requires a backend URL or a hosted transport factory.|
|`@c15t/browser/offline`|The stock UI and client for offline mode only, with bundled policy rules and presets.|
|`@c15t/browser/headless`|The same client with no UI and no CSS. `mountUI()` throws.|
|`@c15t/browser/iab`|The client with the IAB TCF CMP and the IAB banner and dialog. See [IAB TCF](/docs/frameworks/javascript/iab).|
|`@c15t/browser/devtools`|`mountDevTools(client, options)`. See [DevTools](/docs/frameworks/javascript/dev-tools).|
|`@c15t/browser/styles.css`, `@c15t/browser/iab/styles.css`|The stylesheets, for `ui: { shadow: false, styles: false }`.|

Install it with:

|Package manager|Command|
|:--|:--|
|npm|`npm install @c15t/browser@alpha`|
|pnpm|`pnpm add @c15t/browser@alpha`|
|yarn|`yarn add @c15t/browser@alpha`|
|bun|`bun add @c15t/browser@alpha`|

In the ES module build the preference dialog code is a separate chunk. It
starts loading in idle time once the banner or trigger is on screen, and at
the latest when the dialog opens. Idle preloading can download it even when
the visitor never opens preferences.

The script-tag files `c15t.js`, `c15t.offline.js` and `c15t.iab.js` each carry
their complete stylesheet in one file. The default `c15t.js` uses the hosted
client and requires a backend URL or hosted factory. `c15t.offline.js` uses
the offline client. For manifest or custom mode with the stock UI, use the
ES module root `@c15t/browser`, which takes any mode factory.

`@c15t/browser` is not part of the `c15t` package. Use one entry per page.
Each creates its own client, and two clients on one page would each run the
script loader and blockers.

### Choose a bundle for one mode

Use `@c15t/browser/hosted` when Inth or a self-hosted backend supplies the
policy. It excludes offline policy presets, offline resolution and manifest
transport code. Hosted mode already resolves the visitor's policy on the
server through `/init`; this entry removes unused client code without
changing that request.

Use `@c15t/browser/offline` when the browser resolves bundled rules. It
excludes hosted and manifest transport code. The default offline transport
keeps choices in the browser's consent cookie and localStorage, with no
consent backend requests or server records. Not recommended for production
environments.

Both entries keep the banner, preference dialog, page hooks, script gating,
blockers, callbacks and persistence. Their `init()` and
`createConsentClient()` methods have the same lifecycle as the general entry.

|Entry|Accepted configuration|Configuration that throws|
|--|--|--|
|`@c15t/browser/hosted`|`backendURL`, optional `mode: 'hosted'`, or a factory from `hosted()`. `policyRules` accepts authored `PolicyRule` objects.|Missing backend URL without a hosted factory; a different mode; `manifest` or `manifestURL`; preset-name strings in `policyRules`.|
|`@c15t/browser/offline`|Optional `mode: 'offline'` or a factory from `offline()`. `policyRules` accepts authored rules and preset names, or defaults to the recommended rules.|`backendURL`, `manifest` or `manifestURL`; a different mode.|

Use `@c15t/browser` for manifest or custom mode, or when an application
chooses its mode at runtime. Use `@c15t/browser/iab` for IAB TCF. The general
entry imports no transport itself, so a bundle keeps only the factory it
imports. It takes `mode` as a factory only: `backendURL`, `manifest`,
`manifestURL`, `policyRules` and mode names such as `'hosted'` are options of
the `/hosted` and `/offline` entries and the script-tag builds.

## Start the client

`init(options)` creates the client and starts it:

```ts title="src/main.ts"
import { init, manifest } from '@c15t/browser';
import { posthog } from '@c15t/integrations/posthog';

init({
	mode: manifest(),
	scripts: [
		posthog({
			id: 'phc_your_project_key',
			initOptions: { cookieless_mode: 'never' },
			loadMode: 'after-consent',
		}),
	],
});
```

`createConsentClient(options)` creates the client without starting it.
Construction reads no storage, sends no request and touches no DOM, so a
module can create it at import time and call `client.start()` later:

```ts
import { createConsentClient, manifest } from '@c15t/browser';

export const consent = createConsentClient({ mode: manifest(), scripts });
```

`start()` reads stored choices, requests the policy, starts the script loader,
the iframe blocker and any network blocker, listens for page hooks and mounts
the UI. It mounts on `DOMContentLoaded` when `<body>` does not exist yet. A
second `start()` does nothing. [Options](/docs/frameworks/javascript/api/browser-options)
lists everything `init` and `createConsentClient` accept.

## Read state

|Member|Returns|Use it for|
|--|--|--|
|`ready()`|`Promise<ConsentSnapshot>`|Wait until the policy has resolved. For a returning visitor, `hasConsented()` is reliable after it.|
|`has(condition)`|`boolean`|Whether a category, or a condition such as `{ or: ['measurement', 'marketing'] }`, is allowed right now.|
|`hasConsented()`|`boolean`|Whether the visitor has answered the choice prompt.|
|`getSnapshot()`|`ConsentSnapshot`|The full state. See the [snapshot reference](/docs/frameworks/javascript/api/snapshot).|
|`subscribe(listener)`|`() => void`|Call `listener` with each new snapshot. Returns an unsubscribe function.|
|`on(event, listener)`|`() => void`|Listen for `ready`, `consent`, `ui` or `error`.|
|`consentCategories`|`AllConsentNames[]`|The categories the dialog offers, in a fixed order: `necessary`, `functionality`, `measurement`, `experience`, `marketing`, limited to the policy's categories. The order of the `consentCategories` option doesn't change it.|

`has()` reads a permission, which says whether code may run now. Under an opt-out policy
it is `true` before the visitor does anything. `hasConsented()` and
`getSnapshot().explicitChoice` read the recorded choice, which says what the
visitor decided. Gate code on permissions; report on recorded choices.
[How consent works](/docs/concepts/how-consent-works#a-permission-is-not-a-recorded-choice)
explains the difference.

## Record a choice

Call these only from a visitor's click or key press.

|Method|Returns|What it records|
|--|--|--|
|`acceptAll()`|`Promise<SaveResult>`|Without IAB, allows every category the dialog offers. Under an IAB policy it confirms the vendors' declared processing through the CMP. Optional categories without declared consent purposes remain denied. See [categories under an IAB policy](/docs/concepts/consent-state#categories-under-an-iab-policy).|
|`rejectAll()`|`Promise<SaveResult>`|Denies every optional category the dialog offers.|
|`save(consents)`|`Promise<SaveResult>`|The categories you name, such as `{ measurement: true }`. Categories the dialog does not offer are dropped. Under an IAB policy it resolves `{ ok: false }` and emits `error`; use `saveIAB()`.|
|`saveIAB()`|`Promise<SaveResult>`|The IAB purpose and vendor choices on `runtime.iab`. IAB entry only.|
|`dismissNotice()`|`Promise<NoticeDismissResult>`|Acknowledges a notice. Grants nothing. `{ ok: false, reason: 'not-required' }` when no notice is due.|
|`identify(user)`|`Promise<void>`|Links the consent record to `{ externalId, identityProvider?, identityToken? }`; the [token](/docs/self-host/api/node-sdk#verify-a-link-made-in-the-browser) verifies the link. Failures surface through `error`.|

Each save applies in the browser and closes the banner or dialog before the
backend request finishes. `SaveResult.ok` is `false` when the request failed;
the choice stays in effect and c15t retries the request later.

## Control the UI

|Method|What it does|
|--|--|
|`showBanner()`, `openDialog()`, `closeDialog()`|Change the surface c15t wants shown. Records nothing.|
|`setLanguage(code)`|Sets the language and resolves the policy again, which fetches that language's copy from the backend. In offline mode the copy comes from the bundled translations and `i18n.messages`. See [translations](/docs/frameworks/javascript/translations).|
|`setOverrides(overrides)`|Merges into the country, region, language and GPC inputs to policy matching. A key you pass replaces its value, `undefined` clears it, and a key you leave out stays.|
|`mountUI(options?)`|Removes the stock UI and mounts it again with new UI options. Throws in the headless entry.|
|`processIframes()`|Pauses gated iframes that consent does not allow and restores the ones it allows. Needed only with `iframeBlocker: { disableAutomaticBlocking: true }`. See [iframe blocker](/docs/frameworks/javascript/modules/iframe-blocker#check-iframes-on-demand).|
|`ui`|The mounted UI handle, with `host`, `root`, `update()` and `destroy()`, or `null`.|

## Lifecycle and internals

|Member|What it is|
|--|--|
|`start()`|Starts the client. `init()` calls it for you.|
|`started`|Whether `start()` has run and the client is not disposed.|
|`dispose()`|Removes the UI, page listeners, blockers and the scripts c15t added, then disposes the runtime. Code that already ran keeps running.|
|`options`|The options the client was created with.|
|`mode`|`hosted`, `offline`, `manifest` or `custom`.|
|`runtime`|The [consent runtime](/docs/frameworks/javascript/api/runtime) that owns the kernel and modules. `runtime.iab` is the CMP handle under IAB.|
|`kernel`|The [consent kernel](/docs/frameworks/javascript/api/kernel): snapshot, commands and events.|

Do not attach a second script loader, network blocker or iframe blocker to
`client.kernel`. The client already runs one of each.

## Events

`on(event, listener)` returns an unsubscribe function:

|Event|Payload|When it fires|
|--|--|--|
|`ready`|the snapshot|The policy has resolved and the UI can render. For a returning visitor, `hasConsented()` is reliable from here.|
|`consent`|the snapshot|Permissions, the recorded choice or the vendor switches changed. This includes a stored choice restored at start, a save, an expiry and a GPC change.|
|`ui`|`'banner'`, `'dialog'` or `'none'`|The surface c15t wants shown changed.|
|`error`|the error|A backend call failed, or an IAB step failed.|

A `ready` listener added after the policy resolved runs at once with the
snapshot from that moment. A `ui` listener added after that runs at once
with the current surface. `consent` and `error` listeners only hear later
changes.

Each event is also dispatched on `document` as `c15t:ready`, `c15t:consent`,
`c15t:ui` and `c15t:error`, with the payload in `event.detail`.
[Callbacks](/docs/frameworks/javascript/callbacks) compares events with the
`callbacks` option.

## Page hooks

A started client also wires markup on the page, with no code:

|Markup|What it does|
|--|--|
|`<button data-c15t-action="…">`|Runs the action on click. Values below.|
|A link whose `href` ends in `#c15t-preferences`|Opens the preference dialog.|
|`<script type="text/plain" data-c15t-category="…">`|Runs the script once that category is allowed, in page order.|
|`<iframe data-src="…" data-category="…">`|Sets `src` once that category is allowed.|

|Value|What a click does|
|--|--|
|`accept`|Like the banner's accept button, allows every category the dialog offers without IAB. Under an IAB policy it confirms the vendors' declared processing through the CMP. Optional categories without declared consent purposes remain denied. See [categories under an IAB policy](/docs/concepts/consent-state#categories-under-an-iab-policy).|
|`reject`|Rejects every optional category the dialog offers.|
|`customize`|Opens the preference dialog. Records nothing.|
|`dismiss`|Dismisses a notice. This records an acknowledgement, not consent, and does nothing unless the policy shows a notice.|
|`banner`|Shows the banner again. Records nothing.|
|`close`|Closes the banner or dialog. Records nothing.|

## Helper exports

The general `@c15t/browser` entry exports all the helpers below. The hosted
entry exports `hosted`; the offline entry exports `offline` and
`resolveRules`. Both include the page action constants, gated script
helpers, `mountConsentUI` and `version`.

|Export|What it does|
|--|--|
|`hosted`, `offline`, `manifest`, `custom`|Transport factories for the `mode` option. See [transports](/docs/frameworks/javascript/transports).|
|`manifestNeedsLocation(manifest)`|Whether some location gets a different banner, or none, under this manifest, so a page knows it must supply a country to resolve it without `/init`. `false` when every country and region rule gives the same banner.|
|`resolveRules(rules)`|Turns preset names such as `'europeOptIn'` into policy rules. Throws on an unknown name.|
|`activateGatedScripts(snapshot, root?, options?)`|Runs allowed `text/plain` scripts under `root` yourself, for a setup without a client. `options.nonce` runs only tags that carry that nonce. Returns how many ran.|
|`mountConsentUI(client, options?)`|Mounts the stock UI for a client. `mountUI()` calls it.|
|`ACTION_ATTRIBUTE`, `PREFERENCES_HASH`, `CATEGORY_ATTRIBUTE`, `ACTIVATED_ATTRIBUTE`|`'data-c15t-action'`, `'#c15t-preferences'`, `'data-c15t-category'` and `'data-c15t-activated'`.|
|`version`|The package version.|

The package also exports the `ConsentClient`, `ConsentClientOptions`,
`ConsentUIOptions`, `ConsentBannerOptions`, `ConsentDialogOptions`,
`ConsentTriggerOptions`, `ConsentClientEventMap`, `ConsentSnapshot` and
`ConsentState` types.

The hosted and offline entries export `HostedConsentClientOptions` and
`OfflineConsentClientOptions`, respectively. These types restrict connection
options to the entry's mode. Runtime validation also checks a supplied
transport factory's `kind`.

## Check it works

1. Start the app and run `await consent.ready()` from code or a breakpoint.
   It resolves with a snapshot whose `resolution.status` is `matched`.
2. Click **Reject All**. `consent.hasConsented()` is `true` and
   `consent.has('measurement')` is `false`.
3. Call `consent.openDialog()`. The preference dialog opens with
   **Analytics** (the `measurement` category) off.
