---
title: "Install & Browser Runtime"
description: "The one install guide for the Noxtica collector: core and extensions scripts, stable or pinned tags, recorder head, CSP, consent, protected actions, and checks."
canonical_url: "https://noxtica.com/docs/browser-runtime/"
markdown_url: "https://noxtica.com/docs/browser-runtime/index.md"
last_updated: 2026-10-27
published: 2026-01-01
section: "browser-sdk"
section_title: "Browser SDK"
site: "Noxtica"
language: "en"
source: markdown
---

# Install & Browser Runtime

**Find your snippet in the Backoffice → Integrations & API → Script & Consent** ([`/integrations/script-consent`](https://backoffice.noxtica.com/integrations/script-consent)). Pick the domain and the page generates the tags with that domain's Site Key and the current collector build, plus the consent mode, protected actions, and a verification checklist. This page is the public walkthrough of the same content: what to install, in which order, and how to check that it works. Every Site Key on this page is a placeholder (`pk_prod_your_site_key_here`); never copy a key, hash, or build name from a document.

The [developer overview](https://noxtica.com/developers/) links the integration guides in order. Other docs pages link here instead of repeating install steps.

## Current public surface

The public collector loader is served from the Noxtica collector origin, `https://collect.noxtica.com` unless your account has a provisioned collector host. This page calls that origin "your collector host". No public npm package, framework wrapper, native SDK, or cross-platform wrapper passed the 2026-09-20 release check described in [Getting started](https://noxtica.com/docs/getting-started/#before-you-integrate), so those surfaces are omitted rather than inferred from private inventory.

## Install: core on every page, extensions after consent

The collector is two scripts. Load the core on every page; load the extensions only when your consent mode allows it.

- The **core** (`noxtica-collector-core.js`) needs no consent. It obtains the session token, submits the minimal security check, answers server-requested challenges, offers the consent API and the extensions loader, and runs [protected actions](https://noxtica.com/docs/browser-runtime/#protected-actions). In a consent-gated mode it stores nothing on the visitor's device.
- The **extensions** (`noxtica-collector-extensions.js`) need consent. They carry persistent device recognition, the full device fingerprint, analytics, real-user monitoring and console logs, session replay, and behavioral signals. Each module still runs only when its category is granted.

```html
<script
	src="https://collect.noxtica.com/collector/noxtica-collector-core.js"
	data-site-key="pk_prod_your_site_key_here"
	data-auto-init
	data-auto-check-once
	async
></script>
```

| Attribute              | Purpose                                                                                                      |
| ---------------------- | ------------------------------------------------------------------------------------------------------------ |
| `data-site-key`        | The public Site Key of the exact origin. Keys are origin-bound: a token request from another origin is refused with `403` and `{"error":"Origin mismatch"}`. |
| `data-auto-init`       | Creates the client and assigns `window.noxticaClient`.                                                       |
| `data-auto-check-once` | Runs the cache-aware check (`checkOnce()`) once per page.                                                    |
| `data-extensions`      | Optional. `"auto"` makes the core load the extensions itself; any other value keeps that off.                |
| `async`                | The core never needs to block rendering.                                                                     |

Load the extensions in one of three ways:

- add `data-extensions="auto"` to the core tag: the core loads them immediately in `not_required` mode and on the first granted category in the `api` and cookie modes;
- call `await window.noxticaClient.loadExtensions()` after your page grants consent (see [Loading extensions](https://noxtica.com/docs/browser-runtime/#loading-extensions-and-its-result));
- inject `<script src="https://collect.noxtica.com/collector/noxtica-collector-extensions.js" async></script>` yourself after consent. It carries no Site Key and attaches to the core already on the page.

Loading the extensions never grants consent by itself. You can also switch **Auto-load extensions** on per domain in Script & Consent. In `not_required` mode that loads the extensions for every visitor, including those who declined your own banner, so treat it as a decision about your banner, not a convenience.

**Existing installs.** The legacy URLs `https://collect.noxtica.com/collector/noxtica-collector.js` and `/collector/noxtica.js` now serve the core and load the extensions automatically: immediately in `not_required` mode, after the first grant in the gated modes. Set `data-extensions` to any other value to opt out. The original combined bundle is deprecated and no longer offered; new installs use the core tag above.

**Device-analysis engines.** The KHAN and Obsidian engines are split the same way (`khan-core` and `obsidian-core`, then the matching extensions) and load automatically with the core and the extensions. You never add their tags.

## Stable or pinned snippets

Script & Consent generates two variants of each tag from the collector's live manifest, `https://collect.noxtica.com/collector/asset-manifest.json`.

- **Stable (default).** The unhashed URL always serves the current build and carries no `integrity` attribute, so the tag never needs updating.
- **Pinned with Subresource Integrity.** Use it only if your security policy requires an `integrity` attribute. A pinned tag references one exact content-hashed build:

```html
<script
	src="https://collect.noxtica.com/collector/noxtica-collector-core.<hash>.js"
	integrity="sha384-<from the manifest>"
	crossorigin="anonymous"
	data-site-key="pk_prod_your_site_key_here"
	data-auto-init
	data-auto-check-once
	async
></script>
```

Build it from the manifest: `assets["noxtica-collector-core.js"]` is the file name under `/collector/` and `integrity["noxtica-collector-core.js"]` is the `integrity` value. The extensions use the keys `noxtica-collector-extensions.js`. A pinned core loads the extensions with their own integrity hash, so you do not pin the extensions separately.

**How long a pin lasts.** The hashed core and extensions bundles of the last three previous releases keep resolving, byte-identical and immutable, and are listed in `previous[]` of the manifest. Copy the pinned tag again within three releases; after that the pinned file stops being served and the browser blocks it. Hashed files are never a substitute for the manifest: do not copy a hash from a document, including this one.

**The recorder head is the exception.** The early replay loader (`noxtica-recorder-head.js`) is not retained: its hashed URL stops resolving on the next recorder build, so the stable tag in the next section is the default.

## Replay recorder head

Session replay can start before the page paints. The recorder head is a small synchronous preloader that buffers a bounded early window in memory and has no transport. The asynchronous core drains it only after the configured replay admission checks allow capture; otherwise the queue is discarded. Sites that do not use replay may omit it.

```html
<script src="https://collect.noxtica.com/collector/noxtica-recorder-head.js" crossorigin="anonymous" data-cfasync="false"></script>
```

- It must be the **first executable script** in `<head>`, before anything else that runs.
- Load it **synchronously**: never `async`, `defer`, or `type="module"`.
- `data-cfasync="false"` stops Cloudflare Rocket Loader from deferring it.
- It needs an **armed replay policy** for the domain (Backoffice → Session Replay) and a consent flow that calls `setConsent` with the `replay` category.
- A pinned variant comes only from the live manifest key `noxtica-recorder-head.js`; keep `crossorigin="anonymous"` and `data-cfasync="false"`.

## Content Security Policy

If your site sends a Content Security Policy, add only what the collector needs, and merge the sources into your own lists; do not paste a vendor policy over yours. Hosts below are your collector host (`collect.noxtica.com` by default) and the ingest host (`ingest.noxtica.com`, derived automatically from the default collector host).

| Directive     | Allow                                                    | Why                                                                                                                                                    |
| ------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `script-src`  | your collector host and `'wasm-unsafe-eval'`             | Loads the core, extensions, and engine loaders. `'wasm-unsafe-eval'` lets the KHAN and Obsidian engines compile WebAssembly; it does not allow `eval()`. |
| `worker-src`  | `blob:` (keep your own entries, such as `'self'`)        | The engines start workers from `blob:` URLs. A worker script must be same-origin, so it cannot be served from the collector host.                       |
| `connect-src` | your collector host **and** the ingest host (only if your page declares one) | Token and submit calls go to the collector host. Analytics, RUM, console logs, and replay use the ingest host and fail over to the collector once, then stay there; a policy with only one of the two breaks half the traffic. |

If your domain uses server-requested challenges, also allow Cloudflare Turnstile as its documentation describes (`https://challenges.cloudflare.com`). The Script & Consent **CSP & storage** tab prints the same lines with your own hosts filled in.

```http
Content-Security-Policy:
  script-src 'self' https://collect.noxtica.com 'wasm-unsafe-eval';
  worker-src 'self' blob:;
  connect-src 'self' https://collect.noxtica.com https://ingest.noxtica.com;
```

- **`'wasm-unsafe-eval'` is needed.** Without it the engines cannot start; the SDK fails open, collection continues with reduced evidence, and a structured error is reported.
- **`'unsafe-eval'` is not needed.** Noxtica does not need `'unsafe-eval'`, and a policy without it produces no `eval` violation from Noxtica. The only Noxtica code that ever called `new Function(...)` was an optional KHAN diagnostic lane in the extensions runtime: a one-line capability probe, `new Function('return 0')`, and nine diagnostic programs. No score, verdict, or device identifier depends on it. The lane runs only while the signed token arms it, and from release r32 (2026-10-24) it is disarmed for every domain, so the runtime no longer evaluates dynamic code. If a site still sees `eval` reports, it is on an older token or a cached copy: an enforcing `script-src` reports one violation (blocked URI `eval`, source `…/collector/noxtica/noxtica-khan.js`) and a report-only policy about ten per consented visit. Reload with a fresh page load to get a current token; no policy change is needed. Do not add `'unsafe-eval'` for Noxtica.
- **`chrome-extension:` reports are probe noise.** On Chromium, with the `behavioral` category granted, the extension probe requests a few `chrome-extension://…` resources through script tags. An enforcing `script-src` can report them. The probe reads a blocked or missing resource as "not installed", so do not allow `chrome-extension:` for Noxtica.
- **Hash or nonce next to `'unsafe-inline'`.** Browsers that see a hash or nonce ignore `'unsafe-inline'`, which breaks every other inline script. If your policy relies on `'unsafe-inline'`, do not add a hash or nonce for the inline listener without a full nonce migration; if you already use nonces, give the inline listener its nonce.
- **Probe periodically.** A permissive policy hides what it allows: with `'unsafe-inline'` and `'unsafe-eval'` listed, report-only telemetry sees no inline or eval use. Send a stricter policy as a second, report-only header for a day before you tighten the enforcing one, and read the reports. It changes nothing on the page:

```http
Content-Security-Policy-Report-Only: script-src 'self' https://collect.noxtica.com; worker-src 'self'; connect-src 'self' https://collect.noxtica.com https://ingest.noxtica.com; report-uri /csp-report
```

## Consent modes (tenant integration guide)

Consent modes let a domain split the collector into a consent-free core and consent-gated categories. New domains start in `api` mode: the page check is skipped (`noxtica:collected` carries `skipped: 'consent_required'`) and nothing persistent is written until your page calls `setConsent`. Domains that already existed keep the mode they have, so an existing install changes nothing. `not_required` remains an explicit choice that keeps the behavior from before consent modes existed.

### Choosing a mode

Set the mode per domain in **Backoffice → Integrations & API → Script & Consent** (the Consent & extensions tab), stored as the domain's `client_consent_mode`. The collector reads it when it issues the browser token.

| Mode                    | Behavior                                                                                                                                                                                                                       |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `not_required`          | Not a consent gate. Domains created before the `api` default and never changed keep this mode, and you can choose it explicitly. Collection runs as configured today. An explicit `denied` from your page is still honored for that category. |
| `api`                   | The default for new domains. Every category starts denied. Your page grants or denies categories with `client.setConsent(...)`, usually from your consent management platform (CMP).                                                          |
| `cookie:<name>`         | The collector reads the first-party cookie `<name>`. Present and non-empty means every category is granted; absent or empty means every category is denied.                                                                    |
| `cookie:<name>=<value>` | The collector reads cookie `<name>`, URI-decodes it, and grants every category when the decoded value contains `<value>`. Otherwise every category is denied. This matches CMP strings, for example `cookie:OptanonConsent=C0002:1`. |

Cookie names may use letters, digits, `_`, `.`, and `-`; values may additionally use `:`, `%`, and `+`. Backoffice rejects anything else. A value that cannot be parsed is treated as `not_required`, so the domain keeps its current behavior.

**`not_required` is not a consent gate.** The persistent device identifier is written unless your page set `deviceId` to `denied`, and nothing waits for your banner. The extensions write it, so in this mode it applies from the moment they load: right away with `data-extensions="auto"`, the legacy URLs, or **Auto-load extensions**. The core alone never writes persistent storage. Pick `api` or a cookie mode when your visitors must decide first.

### Declaring the mode on the loader (optional)

The domain setting arrives with the first token. If your consent decision reaches the page by API, also declare the mode on the core tag so the client is strict from its first line, before that token returns:

```html
<script
	src="https://collect.noxtica.com/collector/noxtica-collector-core.js"
	data-site-key="pk_prod_your_site_key_here"
	data-consent-mode="api"
	data-auto-init
	data-auto-check-once
	async
></script>
```

Accepted forms are `data-consent-mode="api"`, `data-consent-mode="cookie:name"` or `data-consent-mode="cookie:name=value"`, and the shorthand `data-consent-cookie="name"` or `data-consent-cookie="name=value"`. Programmatic clients pass the same value:

```javascript
const client = NoxticaCollector.createClient({ siteKey: 'pk_prod_your_site_key_here', consentMode: 'api' });
```

A declaration can only make the client stricter. The domain setting stays authoritative for server enforcement, and a tag can never relax it. Without the declaration, a call such as `setConsent` made in the window before the token is announced with `mode: "not_required"`. Up to SDK 3.8.0 the same event then reports `api` only once the token arrives; from SDK 3.8.1 the client dispatches a second `noxtica:consent-changed` with `mode: "api"` when the token resolves, and `getConsent()` and the event detail carry `modeResolved` (`false` until then). If your bridge ever sends a partial object, or calls `loadExtensions()` before `setConsent`, declare the mode.

### Core and extensions in a gated mode

In the `api` and cookie modes, keep the core on every page and load the extensions only after the visitor accepts (see [Install](https://noxtica.com/docs/browser-runtime/#install-core-on-every-page-extensions-after-consent)). With the `api` mode, grant the categories and then load the extensions:

```javascript
const client = window.noxticaClient;
client.setConsent({ deviceId: 'granted', analytics: 'granted', replay: 'granted', behavioral: 'granted' });
await client.loadExtensions(); // resolves { ok: true } once the extensions are active
```

Each value is the string `'granted'` or `'denied'`; any other value, including `true` or `false`, throws a `TypeError`. Adding `data-extensions="auto"` to the core tag makes the core load the extensions on the first granted category instead.

### Consent API

With `data-auto-init`, the client is available as `window.noxticaClient`. Programmatic clients use the object returned by `createClient`.

- `client.setConsent({ analytics, replay, deviceId, behavioral })` sets each listed category to `'granted'` or `'denied'`. Omitted keys keep their current state.
- `client.setConsent('granted' | 'denied')` is the legacy form. It changes `analytics` only.
- `client.setReplayConsent(true | false)` changes `replay` only.
- `client.getConsent()` returns the current state: `{ mode, analytics, replay, deviceId, behavioral, privacySignal }`, where `privacySignal` is `'gpc'`, `'dnt'` or `null`.

Example wiring with a generic CMP callback (OneTrust- or Cookiebot-style). Map your CMP's purposes to Noxtica categories as your privacy team decides:

```javascript
const client = window.noxticaClient;
const state = (allowed) => (allowed ? 'granted' : 'denied');

cmp.onConsentChanged((choices) => {
	client.setConsent({
		analytics: state(choices.statistics),
		replay: state(choices.statistics),
		deviceId: state(choices.preferences),
		behavioral: state(choices.marketing),
	});
});
```

In cookie modes the collector reads the cookie when it starts and again on `visibilitychange` and `focus`, so a decision made in your banner applies when the visitor returns to the tab. A grant applies to all categories at once. A missing or non-matching cookie denies all categories and purges the persistent device stores.

### Verify your consent mode the way the browser sees it

The authoritative mode lives in the domain's tenant settings, not in your page code, so a settings change can alter behavior without any deploy on your side. Check what the browser actually receives:

- `window.noxticaClient.getConsent().mode` reports the effective mode: `not_required`, `api`, `cookie:<name>[=<value>]`, or `gcm`. Read it after the first `noxtica:collected` event, because until the token arrives the client does not know the domain's mode.
- `collectorConfig.consent` in the `POST /client/token` response (DevTools → Network) carries the server-side mode.

Make this a release check on the live page, with the banner still unanswered:

```javascript
// Expect your configured mode ('api' here). 'not_required' means the domain setting changed or the wrong domain is selected.
document.addEventListener('noxtica:collected', () => {
	console.assert(window.noxticaClient.getConsent().mode === 'api', 'Noxtica consent mode drifted');
});
```

With the mode `api` and nothing granted, you should see only the core script and `POST /client/token`, a `sessionStorage` token, and a `noxtica:collected` event with `skipped: 'consent_required'`. Before any decision every category must read `unknown` or `denied`, and `Object.keys(localStorage).filter((k) => k.startsWith('nox_'))` must be empty. Anything in `localStorage`, IndexedDB, or cookies before the visitor answers is a finding.

### Events and timing

Attach your listeners before the loader can run, and treat events as signals, not as data sources.

- **Listen early.** Put the `noxtica:collected` listener in an inline `<head>` script **before** the core tag. `checkOnce()` announces a visit once and does not re-collect inside its TTL, and an event that already fired is not replayed.
- **Deliver consent on `noxtica:ready`** (SDK 3.6.0 and later), which fires once per tag after `window.noxticaClient` exists. Back it with a bounded poll for older cores, and let the latest choice win.
- **Never trust `event.detail.client`.** Any script can dispatch a forged `noxtica:ready`. Use the event as a signal and read `window.noxticaClient`, which the core assigns before it dispatches.
- **Prefer deciding consent in a `noxtica:ready` handler, not in the pre-init queue (`noxtica('setConsent', …)`).** Before SDK 3.8.1 the queue replays every command in order, so a grant followed by a withdrawal still runs `loadExtensions()`. From SDK 3.8.1 queued consent updates are folded into one (the later key wins), a queued `loadExtensions` is dropped when all four categories end denied (`noxtica:extensions-failed` with `consent_missing`), and one `noxtica:consent-changed` fires. Use the queue for one-time declarations either way.
- **Decide "is Noxtica on this page" by host or build configuration,** not by a DOM attribute: a client-side locale switch or framework remount can clear attributes on `<html>`.

```html
<head>
	<script>
		document.addEventListener('noxtica:collected', (event) => {
			const result = event.detail;
			if (result.skipped === 'consent_required') return; // not an error: no device ID yet
			// Use result.fingerprintId only as a lookup handoff to your backend.
		});
	</script>
	<script src="https://collect.noxtica.com/collector/noxtica-collector-core.js" data-site-key="pk_prod_your_site_key_here" data-auto-init data-auto-check-once async></script>
</head>
```

```javascript
// Run when your consent choice is known; calls whenNoxticaReady once the client exists.
function whenNoxticaReady(callback, { timeoutMs = 15000 } = {}) {
	if (window.noxticaClient) return callback(window.noxticaClient);
	const started = Date.now();
	const poll = setInterval(() => {
		if (window.noxticaClient) { clearInterval(poll); callback(window.noxticaClient); }
		else if (Date.now() - started > timeoutMs) clearInterval(poll); // fail open: no client, no collection
	}, 250);
	document.addEventListener('noxtica:ready', () => { clearInterval(poll); if (window.noxticaClient) callback(window.noxticaClient); }, { once: true });
}
```

| Event                       | `event.detail`                                                                     | When                                                                                  |
| --------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| `noxtica:ready`             | `{ client }`                                                                       | `data-auto-init` created the client (SDK 3.6.0+). The first check has not started.   |
| `noxtica:collected`         | the check result, or `{ success: false, skipped: 'consent_required', consentMode }` | After each check, and when a check is deferred until consent.                        |
| `noxtica:cache-hit`         | the same result with `fromCache: true`                                             | Right after `noxtica:collected` when the TTL served the stored result.               |
| `noxtica:consent-changed`   | the `getConsent()` snapshot                                                        | A category really changed.                                                            |
| `noxtica:extensions-loaded` | `{ message: 'attached', ok: true, loaded }`                                        | The extensions took over the client.                                                  |
| `noxtica:extensions-failed` | `{ message: 'not-loaded', ok: false, reason, code }`                               | A load attempt failed. A later `loadExtensions()` retries.                            |
| `noxtica:error`             | `{ source, message }`                                                              | A failure the SDK reported; also stored in `window.noxticaLastError`.                 |

The `extensions-*` event details are objects from SDK 3.8.0 and strings before it.

### Browser handoff sample

This sample loads the core tag and hands your own code one of three states: `received` with a device identifier, `invalid` when the result carries no identifier, or `unavailable` when the collector reports an error. The browser never receives a server credential. Replace `pk_example_replace_me` with the public Site Key of the exact origin.

```html
<script src="https://collect.noxtica.com/collector/noxtica-collector-core.js" data-site-key="pk_example_replace_me" data-extensions="auto" data-auto-init data-auto-check-once async></script>
<script>
  document.addEventListener('noxtica:collected', (event) => {
    const deviceId = event.detail?.fingerprintId;
    document.dispatchEvent(new CustomEvent('app:noxtica-evidence', {
      detail: deviceId ? { state: 'received', deviceId } : { state: 'invalid' },
    }));
  });

  document.addEventListener('noxtica:error', () => {
    document.dispatchEvent(new CustomEvent('app:noxtica-evidence', {
      detail: { state: 'unavailable' },
    }));
  });
</script>
```

The tag includes `data-extensions="auto"`; the attribute table under [Install: core on every page, extensions after consent](https://noxtica.com/docs/browser-runtime/#install-core-on-every-page-extensions-after-consent) says what it does, and loading the extensions never grants consent by itself. Send only the returned device identifier to your backend, as [Backend integration](https://noxtica.com/docs/backend-integration/#maintained-server-sample) shows.

### Loading extensions and its result

`loadExtensions()` never rejects. From SDK 3.8.0 it resolves `{ ok: true, loaded }` on success and `{ ok: false, reason, code }` on failure; up to 3.7.0 it resolved a boolean. An object is always truthy, so test the result, not its existence. Failures do not throw, so a retry guard that only clears on rejection never clears:

```javascript
let extensionsRequested = false;
async function requestExtensions() {
	if (extensionsRequested) return;
	extensionsRequested = true;
	const res = await window.noxticaClient.loadExtensions();
	const ok = res === true || res?.ok === true; // boolean up to 3.7, object from 3.8
	if (!ok) extensionsRequested = false; // allow a retry on the next consent signal
}
```

| `reason`             | Produced when                                                                                                         |
| -------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `consent_missing`    | You passed `{ requireConsent: true }` in a gated mode and no category is granted. Nothing is fetched.                 |
| `csp_blocked`        | The browser reported an enforced CSP violation for the script URL.                                                    |
| `integrity_mismatch` | A pinned file's bytes differ from its `integrity` value.                                                              |
| `network`            | An HTTP error or no response for the script (`http_<status>`, `fetch_failed`).                                        |
| `timeout`            | Neither a load nor an error event within 20 seconds. A later call retries.                                            |
| `already_loaded`     | A `<script>` for the extensions URL is already in the document but nothing registered.                                |
| `unknown`            | Everything else, such as a build mismatch.                                                                            |

An SRI-pinned core never falls back to unpinned code. An unpinned core falls back from the hashed file to the stable name. Treat `reason` as a hint for logging and `ok` as the contract.

### What runs without consent (core)

These parts run in every mode because they protect the page and its visitors:

- browser token issuance;
- `/client/submit` with the minimal sealed-runtime fingerprint;
- anti-automation counters (event counts and timing, no coordinates or key values);
- challenges that the server requests.

Until `deviceId` is granted, the device identifier stays in memory and `sessionStorage` and is not written to `localStorage`, IndexedDB, or cookies.

| Category     | What a grant enables                                                                                                                                   |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `analytics`  | Real-user monitoring (RUM), RUM batches and error detail, console log capture, the interaction tracker, and the durable client event buffer.         |
| `replay`     | Session replay recording and replay chunk upload.                                                                                                      |
| `deviceId`   | Persistent device storage (`localStorage`, IndexedDB, first-party cookie) and the server's persistent device-recognition token.                       |
| `behavioral` | Behavioral biometrics, pointer and touch tracks, geolocation, cookie contents, favicon supercookie detection, the extension probe, and the WebRTC probe. |

A category that is granted still depends on the tenant and domain features you have enabled; consent never switches on a feature your configuration leaves off.

### Server enforcement

When a domain's mode is anything other than `not_required`, the collector enforces consent on its side too. The loader adds an `nxc` query parameter naming the granted categories to each collector request; do not strip it in proxies or rewrite rules. The collector then:

- accepts and discards analytics payloads without `analytics` consent;
- accepts and discards replay payloads without `replay` consent;
- removes behavioral fields from `/client/submit` without `behavioral` consent;
- skips the persistent device-recognition token without `deviceId` consent.

A request without the parameter, for example from an older cached loader, counts as nothing granted. Discarded payloads still receive a normal success response, so the page never sees an error. Domains on `not_required` are not affected.

### Persistent device recognition switched off

If `persistent_device_recognition_enabled` is `false` for the domain, the collector never persists the device identifier in gated modes, even after `deviceId` is granted.

### Legal classification (not legal advice)

> **This table is informational and not legal advice; consult your DPO/counsel.** Classification depends on your jurisdiction, purposes, and configuration.

| Feature                                 | Data accessed                                                                        | Stored on device?                                                        | Purpose                     | Suggested category | Typical legal basis note                                                                                                             |
| --------------------------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------ | --------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
| Browser token                           | Site Key; the server sees IP address and request headers                             | `sessionStorage` token cache for the session                             | Security                    | core               | Security storage is often treated as strictly necessary under ePrivacy Art. 5(3); GDPR Art. 6(1)(f), fraud prevention (Recital 47). |
| Minimal sealed fingerprint (submit)     | Browser and device characteristics read inside the sealed runtime; proof of work    | `sessionStorage` status flag                                             | Fraud prevention            | core               | Reading device information is access under Art. 5(3); whether the strictly-necessary exemption applies needs assessment. GDPR Art. 6(1)(f). |
| Anti-automation counters                | Event counts, trusted-event flags, timing gaps, pressure; no coordinates or keys     | No                                                                       | Bot detection               | core               | GDPR Art. 6(1)(f); confirm the Art. 5(3) position with counsel.                                                                      |
| Server-requested challenge              | Loads Cloudflare Turnstile when the server asks for it                               | No by the collector; see Cloudflare's Turnstile documentation            | Security                    | core               | Security of the requested service; list Cloudflare as a processor or recipient.                                                     |
| Network and edge signals (server side)  | IP address, TLS and HTTP characteristics, rate limits                                | No                                                                       | Fraud prevention            | core               | No terminal access, so Art. 5(3) does not apply; GDPR Art. 6(1)(f).                                                                  |
| Cross-tab coordination                  | Lock and refresh timestamps                                                          | Yes, small `localStorage` keys, no identifier                            | Operation                   | core               | Functional state; strictly-necessary exemption commonly argued.                                                                      |
| Session device identifier               | Device identifier held for the current session                                       | Memory and `sessionStorage`                                              | Fraud prevention            | core               | Session-scoped storage; Art. 5(3) analysis still applies.                                                                            |
| Persistent device identifier            | Device identifier and an integrity canary                                            | Yes: `localStorage`, IndexedDB, first-party cookie (up to 400 days)      | Cross-session recognition   | deviceId           | Storage under Art. 5(3) usually needs consent unless strictly necessary; GDPR Art. 6(1)(a) or (f).                                  |
| RUM, RUM batches, error detail          | Performance and resource timing, redacted URLs, errors                               | No                                                                       | Analytics                   | analytics          | Art. 5(3) consent is typical; some regulators exempt narrow audience measurement.                                                   |
| Console log capture                     | Page `console` output                                                                | No                                                                       | Analytics and support       | analytics          | Consent is typical; logs may contain personal data your page writes.                                                                 |
| Interaction tracker                     | Clicks with target element type, scroll dwell, page views                            | No                                                                       | Analytics                   | analytics          | Art. 5(3) consent is typical.                                                                                                       |
| Durable client buffer                   | Queued analytics events                                                              | Yes, IndexedDB                                                           | Analytics delivery          | analytics          | Storage under Art. 5(3); follows the analytics purpose.                                                                             |
| Session replay and replay preloader     | Page structure, masked inputs, network and console metadata                          | Preloader: memory only; recorder: `sessionStorage` tab identifier        | User experience and support | replay             | Consent is typical (Art. 5(3) and GDPR Art. 6(1)(a)); masking reduces the risk of capturing special-category data.                  |
| Behavioral biometrics                   | Click, scroll, and key timing                                                        | No                                                                       | Fraud prevention            | behavioral         | Can be biometric data under GDPR Art. 4(14) and Art. 9 when used to identify a person; explicit consent, Art. 9(2)(a). US: BIPA. |
| Pointer and touch tracks                | Pointer and touch coordinates                                                        | No                                                                       | Fraud prevention            | behavioral         | GDPR Art. 9 risk as above; US: BIPA.                                                                                                 |
| Geolocation                             | Latitude and longitude, only when the browser permission is already granted          | No                                                                       | Fraud prevention            | behavioral         | Precise location is high-risk data; browser permission is not a GDPR legal basis.                                                  |
| Cookie contents                         | `document.cookie`                                                                    | No                                                                       | Fraud prevention            | behavioral         | Access under Art. 5(3); cookies may hold other vendors' identifiers.                                                                |
| Favicon supercookie detection           | Same-origin favicon cache                                                            | No                                                                       | Fraud prevention            | behavioral         | Access under Art. 5(3).                                                                                                             |
| Extension probe                         | Presence of browser extensions                                                       | No                                                                       | Fraud prevention            | behavioral         | Access under Art. 5(3); extensions can reveal sensitive traits (Art. 9 risk).                                                       |
| WebRTC probe                            | ICE candidates through a Google STUN server; public IP addresses are HMAC-hashed     | No                                                                       | VPN and proxy detection     | behavioral         | Third-party STUN contact; GDPR Art. 6(1)(f) with a balancing test.                                                                  |

Live assist is loaded separately and asks for its own consent; it is outside these categories.

## Protected actions

Protected actions keep bot and fraud protection on the steps attackers target, even for visitors who have not consented. Examples are `login`, `signup`, `checkout`, `kyc`, and `password_reset`. When one runs, the core executes the KHAN and Obsidian cores in minimal mode: they read device and browser characteristics only, store nothing on the device, and run no behavioral, replay, or analytics collection. Ordinary page views are never exempt.

**1. Declare** each action per domain in Script & Consent (Protected actions tab): a name (lowercase, up to 32 characters) and exactly one trigger, matched by `path` glob (`/login`, `/checkout/**`), by CSS `selector` of a form or button, or by a window `CustomEvent` name. A domain has at most 16 actions. Avoid catch-all paths such as `/**`: they would run the minimal check on every page view.

**2. Trigger** from the page, either on the form or in code:

```html
<form id="login" data-nox-protect="login" method="post" action="/session">…</form>
```

```javascript
const { requestId } = await window.noxticaClient.protect('login');
```

On a native, navigating form submit the core holds the submit for at most 6 seconds and adds a hidden `nox_request_id` field; a slow or failed check never blocks the visitor. For script-submitted forms see [Single-page apps and frameworks](https://noxtica.com/docs/browser-runtime/#single-page-apps-and-frameworks).

**3. Verify on your server.** Send the `requestId` to your backend with the protected request and redeem it from the backend, never from the browser. The call needs a Server API key (`sk_…`) with the `read:device` scope. The id is single-use inside a 300-second window.

```bash
curl -X POST "https://collect.noxtica.com/v1/protected-actions/verify" \
  -H "Authorization: Bearer sk_prod_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"requestId":"REQUEST_ID","action":"login","expectedDomain":"www.example.com"}'
```

| Status | Meaning                                                                                  |
| ------ | ---------------------------------------------------------------------------------------- |
| `200`  | Redeemed. The body carries `fingerprintId`, `verdict`, `score`, and `confidence`.        |
| `400`  | Malformed body: `invalid_body`, `invalid_request_id`, `invalid_action` or `invalid_expected_domain`. |
| `401`  | Missing or invalid key.                                                                  |
| `403`  | The key lacks the `read:device` scope.                                                   |
| `404`  | Unknown, expired (over 300 seconds), or another tenant's id.                             |
| `409`  | `already_consumed`: a replay inside the window. Refuse.                                  |
| `413`  | `payload_too_large`: the body is over 2048 bytes.                                        |
| `422`  | `action_mismatch` or `domain_mismatch`. The receipt is not consumed.                     |
| `429`  | Rate limit exceeded. Wait for `Retry-After`.                                             |

A missing `requestId` (timeout, blocked script) means "no client signal", not a pass: apply your own step-up. Use the `fingerprintId` from the verify response as the authoritative device identifier for that action, not an identifier the browser sent in a header. Up to SDK 3.8.0, two `noxtica:collected` events with different `fingerprintId` values could fire on one page (a minimal check by the core and a full one by the extensions when consent arrived while the token was still loading); from SDK 3.8.1 one page load produces one submit and one `noxtica:collected`. Identifiers can still differ between runtimes, so a client-sent value is not a source of truth.

## Single-page apps and frameworks

- **Load the core once**, in the app shell, never per route or per component. A route change does not need a new tag.
- **`data-auto-check-once` is TTL-cached.** `checkOnce()` respects the configured interval and announces a visit once, so a client-side navigation does not re-collect. Listen for `noxtica:collected` before the loader runs, as described in [Events and timing](https://noxtica.com/docs/browser-runtime/#events-and-timing).
- **Script-submitted forms** (React, SPA, `fetch`) cannot use the hidden `nox_request_id` field, because your own handler calls `preventDefault()`. A capture-phase listener starts the protected check before your handler and publishes the result on `form.dataset.noxRequestId`. Read it synchronously at the top of the handler, and call `protect()` synchronously to join the running check if it is not there yet:

```javascript
async function onSubmit(event) {
	event.preventDefault();
	const form = event.currentTarget;
	// Both lines run before the first await: the id is withdrawn when this handler returns.
	const ready = form.dataset.noxRequestId;
	const pending = ready ? null : window.noxticaClient.protect('login', { timeoutMs: 6000 });
	const requestId = ready ?? (await pending).requestId; // undefined on timeout or blocked script
	await fetch('/api/login', {
		method: 'POST',
		headers: { 'Content-Type': 'application/json', 'X-Request-Id': requestId ?? '' },
		body: JSON.stringify(payload),
	});
}
```

The id is attempt-scoped because it is single-use on the server: a retry after a wrong password runs a fresh check. `await noxticaClient.protect(name)` alone, without `data-nox-protect`, is the right tool for buttons, route changes, and flows without a `<form>`.

- **Angular and Zone.js.** The collector's own listeners, observers, and timers resolve the native functions that Zone.js leaves on the prototypes, so they do not trigger change detection. The session-replay recorder's internal listeners are third-party and are not patchable. If that matters, load the core outside the Angular zone:

```typescript
constructor(private zone: NgZone) {}
ngOnInit() {
	this.zone.runOutsideAngular(() => {
		const s = document.createElement('script');
		s.src = 'https://collect.noxtica.com/collector/noxtica-collector-core.js';
		s.dataset.siteKey = 'pk_prod_your_site_key_here';
		s.setAttribute('data-auto-init', '');
		document.head.appendChild(s);
	});
}
```

### Load timing and weight

A third-party script on every page needs a performance budget, and the load point is a measurement, not a default. Measured on 2026-10-24 at SDK 3.8.0 as served (3.8.1 bytes differ slightly): the core is about 460 KB raw (about 200 KB gzip) and the extensions about 730 KB raw (about 305 KB gzip). After consent the extensions start two WebAssembly modules and dozens of short-lived blob workers (38 in one measured integration). Measure your own page; sizes change with every release.

If the core competes with first paint or hydration, inject it after `load` and the first idle period instead of in `<head>`. Safari has no `requestIdleCallback`, so fall back to `setTimeout`:

```javascript
function loadNoxticaCore() {
	const s = document.createElement('script');
	s.src = 'https://collect.noxtica.com/collector/noxtica-collector-core.js';
	s.async = true;
	s.dataset.siteKey = 'pk_prod_your_site_key_here';
	s.setAttribute('data-auto-init', '');
	s.setAttribute('data-auto-check-once', '');
	document.head.appendChild(s);
}
const afterIdle = (fn) => ('requestIdleCallback' in window ? requestIdleCallback(fn, { timeout: 4000 }) : setTimeout(fn, 0));
if (document.readyState === 'complete') afterIdle(loadNoxticaCore);
else addEventListener('load', () => afterIdle(loadNoxticaCore), { once: true });
```

The core still parses as one main-thread task when it arrives. Keep the listener inline in `<head>` before this loader, keep the recorder head first if you use replay, and make sure the core is loaded before any protected action can fire.

## Storage and cookie disclosure

Name what the collector writes in your cookie and privacy notices. Persistent keys are written by the **extensions** only, only under the `deviceId` category, and never by the core; in a gated mode nothing persistent is written before `deviceId` is granted, and withdrawing it deletes the three device keys. `<sk>` is your Site Key.

| Name                               | Store                                                         | Lifetime                              | Category                | Written by                              |
| ---------------------------------- | ------------------------------------------------------------- | ------------------------------------- | ----------------------- | --------------------------------------- |
| `nox_fp_<sk>`                      | first-party cookie, `localStorage`, IndexedDB `nox_kv_<sk>`   | 400 days                              | `deviceId`              | Extensions                              |
| `nox_itp_canary_<sk>`              | first-party cookie, `localStorage`, IndexedDB `nox_kv_<sk>`   | 400 days                              | `deviceId`              | Extensions                              |
| `nox_tri_<sk>`                     | first-party cookie, `localStorage`, IndexedDB `nox_kv_<sk>`   | 400 days                              | `deviceId`              | Extensions                              |
| `nox_lock_<sk>`                    | `localStorage` (cross-tab lock)                               | at most 30 seconds, removed after each check | `deviceId`       | Extensions, only while storage is open  |
| `noxtica.session.<sk>.token`       | `sessionStorage` (token cache)                                | the browser session                   | none (security)         | Core and extensions                     |
| `noxtica.session.<sk>.protect.<action>` | `sessionStorage` (protected-action hand-off)             | the browser session                   | none (security)         | Core                                    |
| `noxtica.session.khanvm`, `.gpuvm` | `sessionStorage` (engine load status)                         | the browser session                   | none                    | Extensions                              |
| `__noxtica_khan_accel_v1`          | `sessionStorage` (capability cache)                           | the browser session                   | none                    | Extensions                              |
| `nx_replay_tab_id`, `nx_replay_session_v1` | `sessionStorage` (replay continuity)                  | tab session, ends after 30 minutes idle by default | `replay`   | Extensions                              |
| `nox_buf_<sk>`                     | IndexedDB (durable buffer for RUM and console-log events)      | undelivered events dropped after 72 hours | `analytics`         | Extensions                              |
| `_nox_test`, `cookietest`          | capability probes, created and removed in the same call       | none                                  | none                    | Extensions                              |

The three device cookies are set from script with `Path=/`, no `Domain` attribute (host-only), `SameSite=Lax`, and `Secure` on https, for 400 days. They are **not** `HttpOnly`: they are script-written by design, because the cookie is one redundant layer the SDK reads back to restore the other layers after a browser purges them. From SDK 3.8.1 the `nox_fp_<sk>` cookie holds only the state needed to recover the device identifier and its cache lifetime, as URI-encoded JSON: `{"_stateSchemaVersion":2,"deviceId":"…","lastSubmittedAt":"<ISO time>"}`. The last check result (score, risk level, confidence, flags) is written only to `localStorage` and IndexedDB (`nox_kv_<sk>`), never to the cookie; if only the cookie survives a browser purge, the identifier is restored and a cache hit reports no cached result until the next check. Before 3.8.1 the cookie also carried the last check result. Browsers may shorten script-written cookie lifetimes, so treat 400 days as the maximum.

## Check that it works

1. The core sets `window.noxticaClient`. Run `typeof window.noxticaClient` in the console.
2. The Network tab shows `POST /client/token` to your collector host with status 200. Filter by the collector host.
3. The `noxtica:collected` event fires (register your listener before the loader; afterwards read `window.noxticaResult`). In a gated mode before consent it carries `skipped: 'consent_required'`.
4. The Backoffice shows the visit: the domain page lists recent events, and live monitoring shows the device within a minute or two.
5. Server side, `GET https://collect.noxtica.com/v1/device/DEVICE_ID` with `Authorization: Bearer sk_prod_your_api_key` returns the record for the `fingerprintId` from the event. Handle `401`, `403`, `404`, and `429` explicitly.
6. Replay only: `window.__noxticaHead` is present, the replay policy is armed, and the `replay` category is granted.

| Symptom                                               | Likely cause                                                                                                              |
| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `window.__noxticaHead` appears only after first paint | Cloudflare Rocket Loader deferred the recorder head. Add `data-cfasync="false"` and keep it first and synchronous.        |
| Pinned tag blocked with an integrity error            | A stale pin: the file left the retention window or you copied a hash from a document. Re-copy from Script & Consent.      |
| `noxtica:collected` has `skipped: 'consent_required'` | The domain is in a gated mode and `setConsent` was never called, or `deviceId` is not granted.                            |
| `403` with `Origin mismatch` on `/client/token`       | The Site Key belongs to another origin. Use the key of the exact origin the page runs on. |
| Violations in the console                             | A CSP directive from the table above is missing. See [Content Security Policy](https://noxtica.com/docs/browser-runtime/#content-security-policy).                 |

## Release checks, testing and versions

**Guard the Site Key map as a production switch.** If your code selects the Site Key by host, that map is the only thing that turns Noxtica on. A change that removes a key switches it off silently. Add a post-deploy smoke test that the served HTML contains the install tag for each production host, give the files that hold the map and the consent bridge a CODEOWNERS entry, and keep one key per origin set.

**Test against a lab page, not production traffic.** Headless browsers are bot-scored and can be answered by a CDN challenge, so risk scores from headless runs are noise: never compare them across configurations. To test an enforcing CSP or consent flow on the real origin, serve a minimal page that contains your tag from the browser's own request interception, so the Site Key is accepted without sending traffic through your CDN. A spec outline:

- with consent withheld, the only collector traffic is the core script and `POST /client/token`, and storage holds only the `sessionStorage` token;
- after you deliver consent, the event sequence is `noxtica:consent-changed`, `noxtica:extensions-loaded`, then `noxtica:collected` with a `fingerprintId`;
- a blocked extensions script yields `noxtica:extensions-failed`, and the next consent signal retries.

**Versions.** The loader version appears in the manifest `version` field, the header comment of the script, and `window.noxtica.v` (SDK 3.7.0 and later). Behavior has changed between releases: `noxtica:ready` and the pre-init queue arrived in 3.6.0, the session API in 3.7.0, and the object result of `loadExtensions()` with object-valued extension events in 3.8.0. Re-run your release check after each collector release. Where your account has the typed `@noxtica/sdk` package, write against its typings rather than against prose.

## Browser responsibility

The browser loads the collector for the provisioned origin and public Site Key. It reports a collection result or an error state. If a result contains a device identifier, application code may pass that identifier to the customer backend for an authenticated lookup.

A server credential never belongs in browser JavaScript, HTML, a mobile application resource, a URL, or a client-visible log.

## Capability and consent boundary

Evidence availability depends on the current browser, tenant configuration, privacy signals, and consent state. JavaScript disabled or blocked means no new browser assessment. Reduced evidence, a suppressed browser-visible result, and a transport error are different states; none is automatically low risk.

Optional behavioral or replay capture requires its separately reviewed eligibility and consent or lawful-basis path. The public loader alone does not authorize collection beyond the configuration approved for the evaluation.

## Reliability checks

- Register both completion and error handling before relying on a result.
- Keep collection outside the page-rendering critical path.
- Preserve the normal customer journey when collection cannot run.
- Fail open with bounded waits. From consent to a collected result can take 8 to 11 seconds on a cold device; a budget of a second or two will usually expire on a first consented visit. Prefer the server-side verify response for the authoritative identifier over waiting in the browser.
- Send only the returned device identifier to your backend.
- Do not put identifiers, raw results, or credentials in URLs or optional analytics.
- Test the browser versions and privacy modes your own application supports.

## Next step

Follow [Backend Integration](https://noxtica.com/docs/backend-integration/) for authenticated lookup states and [Integration Flow](https://noxtica.com/docs/integration-flow/) for customer-owned policy rollout.
