Documentation for integrators
Install Biscotti, wait for SDK readiness and connect your own integrations to the visitor’s choice. These instructions describe the deployed API v1.
HTML installation
In the panel, add your exact host, install the code, verify the installation and publish the configuration. Copy your site’s code from Installation. Replace YOUR_SITE_KEY below with its public key; use your platform origin (https://biscotti.dev for production, https://dev.biscotti.dev for the separate dev platform). A public key is not an administrative credential.
Put one SDK script in head, before integrations that need consent. Choose one installation method and one key per document. A normal active script can run before the deferred SDK: mark consent-dependent scripts as inert. Keep a permanent cookie settings control in your footer.
Automatic updates (recommended)
Paste the code once. Script fixes arrive automatically, without replacing the code manually. Existing installations and WordPress work the same way.
<script id="biscotti-sdk" src="https://biscotti.dev/consent.js" data-site-key="YOUR_SITE_KEY" defer></script>Pinned version with SRI (advanced)
You choose a specific script version. It receives no automatic fixes, including security fixes. To update it, you must replace the code manually. Published banner settings are fetched automatically in both modes.
<script id="biscotti-sdk" src="https://biscotti.dev/sdk/9c2bc03275827be7431adcc709a295afb1f7778553dbaf3c3c4f8a2df233f702/consent.js" integrity="sha384-VtJRmbHcw1tU2ZG0b1SkNpDBvF0Wk2fuFyZG/CNH7stj+z9FgY8pbYK6eyk5KqmD" crossorigin="anonymous" data-site-key="YOUR_SITE_KEY" defer></script>WordPress
Download the ZIP, install and activate it in WordPress. In Biscotti → Installation → WordPress, copy the platform URL and public key into the plugin. An administrator can generate a temporary, single-use pairing code to connect the plugin. Keep pairing codes and connection credentials private; never put them in page HTML.
Connect the exact WordPress host, publish the configuration and remove duplicate manual SDK installations. Pairing and importing settings do not publish a draft or automatically block every third-party plugin. Check the actual scripts used by your theme and plugins.
Download · WordPressGoogle Tag Manager — Basic
Install Biscotti directly or through WordPress, outside the container. Remove the standard GTM snippet and its noscript iframe. Put this loader below Biscotti and replace GTM-XXXXXXX. It waits for analytics consent; a mixed container still needs consent checks for every tag.
Paste the downloaded template into a native GTM tag template’s Code tab, not Custom HTML. Allow access_consent write for all seven signals below; allow access_globals execution of Biscotti.onConsentChange, Biscotti.canActivate and dataLayer.push, plus dataLayer read/write. Run it on Consent Initialization – All Pages. Add the appropriate additional consent requirements to each tag and test in Preview / Tag Assistant. The template emits biscotti_consent_update in dataLayer; this differs from the SDK’s biscotti:consentchange DOM event. Rule-based tags also need a positive canActivate gate for the published rule. The template is not in Google’s Community Template Gallery.
<script type="text/plain" data-consent-category="analytics">
(function () {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({ 'gtm.start': Date.now(), event: 'gtm.js' });
var script = document.createElement('script');
script.async = true;
script.src = 'https://www.googletagmanager.com/gtm.js?id=GTM-XXXXXXX';
document.head.appendChild(script);
})();
</script>Download · GTMAPI and readiness
window.Biscotti is installed when the script executes. Its load event and presence do not mean ready. Configuration loading starts before DOMContentLoaded; UI waits for the DOM. Ready means a validated publication, resolved initial preferences, updated gates and Consent Mode, UI and cross-tab listeners. No decision and a saved refusal are valid ready states.
Readiness does not wait for audit delivery, pageviews, fonts or external tags. Restored adapters start after the initial DOM events; Promise callbacks may run after synchronous adapters. ready and error are terminal initialization states for this document. A runtime error does not turn ready into error. After an initialization failure, later valid choices still emit consent changes; whenReady stays rejected until a new document.
| Method | Behaviour |
|---|---|
apiVersion | Constant 1: integration contract version. |
openPreferences() | Opens preferences; queues the request during loading. No consent is granted. |
getConsent() | Returns a fresh mutable copy of six booleans; necessary is always true. |
canActivate(ruleId) | Returns the current positive gate for a published ruleId. Unknown, paused or unavailable rules return false. |
onConsentChange(callback) | Immediately calls callback with a copy, then on applied preferences; may repeat states. Returns unsubscribe; callback exceptions are isolated. |
getStatus() | Returns loading, ready or error for initialization, not the visitor’s decision. |
whenReady() | Promise of the frozen initial snapshot, replayed to late callers; rejects after initialization failure. |
getLastError() | Latest frozen error detail, or null. A successful choice does not clear history. |
DOM events and payloads
Listen on window, before the SDK script if you need the initial events. biscotti:ready fires once; biscotti:consentchange carries a snapshot and reason; biscotti:error adds code and phase. They are CustomEvent objects, with bubbles=false and cancelable=false. Late listeners do not receive past events; use whenReady, getConsent and getLastError.
Successful start: state/GCM/UI → legacy callbacks → ready → initial consentchange → restored adapters. Later choice: state → callbacks → consentchange → activation or required reload. Failure: optional categories denied → consentchange if changed → error; initialization failure never emits ready. Snapshots and nested consents are frozen and contain no decision ID, visitor URL, cookie value or raw exception. hasDecision distinguishes an explicit refusal from no valid choice. Publication metadata is null before validation. Listener exceptions do not stop enforcement. Repeated audit acknowledgements do not duplicate DOM changes.
W17 — biscotti:beforereload precedes required reload/replace after withdrawal. BeforeReloadDetail = SdkSnapshot + reason + navigation. Synchronous listener code on window runs before navigation. The event cannot cancel navigation; the SDK does not await asynchronous work. See the withdrawal section for ordering and an example.
window.addEventListener('biscotti:ready', ({ detail }) => {
document.documentElement.dataset.biscottiReady = String(detail.apiVersion);
});
window.addEventListener('biscotti:consentchange', ({ detail }) => {
document.documentElement.dataset.analyticsConsent = String(detail.consents.analytics);
});
window.addEventListener('biscotti:error', ({ detail }) => {
console.warn('Biscotti', detail.code, detail.phase);
});type SdkSnapshot = {
readonly apiVersion: 1;
readonly siteKey: string | null;
readonly publicationVersion: number | null;
readonly consentEpoch: number | null;
readonly environment?: 'development';
readonly hasDecision: boolean;
readonly consents: Readonly<{
necessary: true;
functional: boolean; analytics: boolean; performance: boolean;
advertising: boolean; unclassified: boolean;
}>;
};
type ConsentChangeDetail = SdkSnapshot & {
readonly reason: 'initial' | 'choice' | 'storage' | 'expiry' | 'error';
};
type BeforeReloadDetail = SdkSnapshot & {
readonly reason: ConsentChangeDetail['reason'];
readonly navigation: 'reload' | 'replace';
};
type SdkErrorDetail = SdkSnapshot & {
readonly code:
| 'INVALID_SITE_KEY' | 'CONFIGURATION_UNAVAILABLE'
| 'INVALID_CONFIGURATION' | 'STORAGE_UNAVAILABLE'
| 'CONSENT_MODE_FAILED' | 'ADAPTER_FAILED'
| 'SYNC_FAILED' | 'INITIALIZATION_FAILED';
readonly phase: 'initialization' | 'runtime';
};Single-page applications
Run the example after the SDK script element exists in the DOM; its file may still be loading or may already have loaded. Install the SDK once in the document shell. Connect subscriptions whether initialization succeeds or fails; never put onConsentChange only inside a successful whenReady callback. This complete example keeps a DOM data attribute in sync. Call disconnectBiscotti when unmounting. Replace the attribute update with your own consent-aware integration.
whenReady always returns the initial snapshot, even after later choices. Use getConsent or a subscription for current preferences. Navigation within the same document does not reinitialize the SDK or fetch a new publication. The SDK can activate newly inserted inert elements once per element. Do not replace window.Biscotti with a stub or method queue.
function connectBiscotti() {
const api = window.Biscotti;
if (!api || api.apiVersion !== 1) return () => {};
const unsubscribe = api.onConsentChange(consents => {
document.documentElement.dataset.analyticsConsent = String(consents.analytics);
});
void api.whenReady().catch(error => {
console.warn('Biscotti initialization', error.code);
});
return unsubscribe;
}
const script = document.getElementById('biscotti-sdk');
let disconnect = () => {};
const onLoad = () => { disconnect(); disconnect = connectBiscotti(); };
if (window.Biscotti) onLoad();
else script?.addEventListener('load', onLoad, { once: true });
const onError = () => { document.documentElement.dataset.analyticsConsent = 'false'; };
script?.addEventListener('error', onError);
function disconnectBiscotti() {
script?.removeEventListener('load', onLoad);
script?.removeEventListener('error', onError);
disconnect();
}Inert script adapters
Use type="text/plain", data-consent-category and, for external scripts, data-consent-src instead of src. Replace the example URL with your integration URL. Enabled optional categories begin denied. An adapter runs once per element per document after the required consent.
Supported: classic inline and external HTTP(S) scripts, with nonce, integrity and crossorigin copied to the activated script. JavaScript modules, nomodule scripts, noscript iframes, arbitrary pixels and server-set cookies are outside this adapter contract. A category adapter is independent of published blocking-rule pause; canActivate(ruleId) checks the published rule and does not start a tag. Review mixed-purpose tags individually.
<script type="text/plain" data-consent-category="analytics">
window.exampleAnalyticsStarted = true;
</script>
<script type="text/plain" data-consent-category="analytics"
data-consent-src="https://analytics.example.com/sdk.js"></script>Categories and Google Consent Mode
The SDK sets optional Google signals to denied and security_storage to granted, then updates the mapped signals from the choice. The current integration is Basic: Google tags must wait for consent. It does not install GA/GTM or enable Advanced mode automatically.
The table shows the exact mapping. performance and unclassified have no native Google signal: enforce their own category or positive published-rule gate. A missing signal is not permission to start a tag. Disabled categories cannot be granted. For GTM, require additional consent checks as well as rule gates where applicable.
performance covers technical measurement: page speed and site stability. Classify traffic measurement, visitor behaviour, GA4 and Clarity as analytics; campaign measurement and remarketing as advertising, even when the tool also measures speed. Classify by actual purpose; split tools with multiple purposes or require all relevant consents. performance and unclassified have no native Google signal. performance alone grants neither analytics_storage nor advertising consent. Gate performance scripts on that category or a positive published-rule gate; a missing Google signal is not permission. In GTM require biscottiPerformance equals true, plus the positive rule gate for published rules. Do not invent performance_storage. The mapping and meaning of saved consents remain unchanged; this correction needs no new decision. If you change a tool’s purpose or consent scope, publish with consent renewal.
| Category | Behaviour | Google Consent Mode |
|---|---|---|
necessary | Core website functions, security and remembering your decision. Always active. | security_storage = granted |
functional | Additional features and remembering visitor preferences. | functionality_storage, personalization_storage |
analytics | Measuring traffic and understanding how the website is used. | analytics_storage |
advertising | Personalizing ads and measuring campaign effectiveness. | ad_storage, ad_user_data, ad_personalization |
performance | Measuring website speed, stability and performance. | No native Google signal |
unclassified | Other cookies whose purpose still needs to be determined. | No native Google signal |
Domains and test environments
A publication matches one exact hostname. example.com, www.example.com and shop.example.com are different hosts. SDK validation and browser CORS compare the publication host; the site key does not bypass this. Preferences are host-only and are not shared automatically between subdomains.
A separate key for localhost and staging tests. Test data stays out of production reports and billing. Save the banner draft and approve cookie disclosures first. The test copy uses these and the latest published blocking rules (none before publication). Each save renews test consent. Localhost, 127.0.0.1 and [::1] require the exact port. Staging requires HTTPS on port 443 and installation verification with the test key. The production host is excluded.
In the site panel choose Installation → Developer mode → Save test configuration, add http://localhost:5173 and copy the dev_ key snippet. Run the example below on that origin, using the hosted Biscotti platform. You do not need to host Biscotti locally. Production data and its site_ key stay separate. Add staging as an exact HTTPS origin, put the test snippet on its home page, then select Check installation. Verification never connects to private addresses.
Test preferences and the outbox are separated by key and exact origin, including port. SDK snapshots and events add environment: development; the banner shows a test notice. Disabling, removing an origin or rotating the key blocks future requests; reload existing documents. The test key is public. Origin checks restrict browsers, not authenticate server requests. After consent, adapters and tags still run real integrations: use your own test accounts. This creates no additional licensed site or linked staging site. Rotation deletes test data only and requires staging verification again.
Test limits: 10 origins, 20 configurations, 1000 decisions and 1000 pageviews. Rotate the key to reset.
Public staging must be a proper subdomain of the production domain, e.g. staging.example.com for example.com or www.example.com (only one leading www. is omitted); add a separate site to test on another domain.
<script>
window.addEventListener('biscotti:ready', ({ detail }) => {
console.assert(detail.environment === 'development');
});
</script>
<script src="https://biscotti.dev/consent.js" data-site-key="YOUR_TEST_KEY" defer></script>
<script type="text/plain" data-consent-category="analytics">
document.documentElement.dataset.testAnalytics = 'active';
</script>
<button type="button" data-biscotti-preferences>Cookies</button>Changing and withdrawing consent
Keep this control in the site footer. openPreferences opens settings without granting consent. Once SDK adapters have run, even partial withdrawal requires a document reload to remove their running code.
Listen on window for biscotti:beforereload. Its frozen BeforeReloadDetail contains the current SdkSnapshot, reason (choice, storage, expiry or error) and navigation (reload or replace). For an explicit choice, persistence, Consent Mode updates, closed gates and consentchange precede this event. Emergency denial first denies locally, attempts to update Consent Mode and clear preferences (cross-tab synchronization does not erase another tab’s choice), emits consentchange if changed and any error, then beforereload and replace with forced denial. If Consent Mode fails, gates stay closed and error reports the failure.
Save application state synchronously, for example a view identifier in sessionStorage. The event is not cancellable; preventDefault, listener exceptions and returned Promises do not cancel navigation. The SDK does not await asynchronous work. Synchronous listener code runs before navigation. The signal covers local changes, another tab, expiry and emergency denial; it does not fire when no reload is required. Backend cookies and processing still need their own checks. Do not treat an unsigned cookie as authentication.
If you choose Stay in the browser’s beforeunload dialog, a later withdrawal emits the signal again and retries navigation.
<button type="button" data-biscotti-preferences>Cookie settings</button>window.addEventListener('biscotti:beforereload', ({ detail }) => {
try {
sessionStorage.setItem('app:view', document.documentElement.dataset.currentView ?? 'home');
} catch { /* Storage may be unavailable; consent withdrawal still proceeds. */ }
document.documentElement.dataset.biscottiReloadReason = detail.reason;
});CSP, CORS and integrity
Allow the platform origin in script-src and connect-src, and /brand/fonts assets in font-src. Authorize your selected external tags too. Inline inert adapters need an appropriate CSP nonce or hash; the SDK copies the script nonce. The current SDK creates a style element in its Shadow DOM without propagating a style nonce. A strict policy must account for this style, or the banner may be unstyled; copying a script nonce alone does not solve it. There is no universal CSP header to paste for every site.
Browser CORS accepts the publication host and uses credentials omitted. It is not server-to-server authentication. Paste the code once. Script fixes arrive automatically, without replacing the code manually. Existing installations and WordPress work the same way. Automatic updates (recommended): /consent.js. Pinned version with SRI (advanced): /sdk/<SHA-256>/consent.js, integrity SHA-384, crossorigin="anonymous". Public SDK files and manifests allow CORS *, while API requests still require the site host. SRI verifies the JavaScript file, not API configuration, fonts or external tags. /consent.js remains a mutable compatibility URL for existing installations and WordPress; do not pin SRI to it.
The banner can use validated configuration in localStorage for 15 minutes, scoped to platform, key and origin. Every document confirms the current publication and host with the server; ETag/304 avoids transferring unchanged configuration again. Acceptance and optional scripts stay disabled until confirmation, while rejection works immediately. Rejection during an outage clears the local grant; its audit record needs a current configuration. Publication or consentEpoch changes return fresh configuration; withdrawal, removed hosts, invalid responses and network failures clear the cache and deny grants. The cache adds no authorization delay in a new document; requests time out after 8 seconds and remain denied. An already open document keeps its snapshot until navigation, without background polling, so there is no propagation deadline for a continuously open tab. A first visit without cache still waits for the server. dev_ bypasses this cache. No installation code update is needed.
Errors and verification
Use stable codes, not Error.message text. whenReady rejects with BiscottiInitializationError on initialization failure. getLastError preserves the latest error even after a later successful choice. phase is initialization or runtime, independent of the terminal status. Failure denies optional categories. Script download failures must be handled on the script element itself.
Check DevTools Network and Console: script, config host/key/CORS, CSP, storage and the published category. Test a fresh visit, refusal, partial and full consent, return visit, a second tab, withdrawal, offline API and failed storage. Verify no tag starts before consent or after withdrawal. Test real GTM/WordPress tags separately; the SDK cannot verify the behaviour of every third-party integration.
code | Behaviour |
|---|---|
INVALID_SITE_KEY | Missing or malformed data-site-key. |
CONFIGURATION_UNAVAILABLE | Network, timeout, response-size or non-2xx configuration response. |
INVALID_CONFIGURATION | Invalid JSON/schema, site key or hostname mismatch. |
STORAGE_UNAVAILABLE | Storage/cookie cannot be read or written, or the preference envelope is damaged. |
CONSENT_MODE_FAILED | Consent commands could not reach dataLayer. |
ADAPTER_FAILED | Adapter activation failed or timed out. |
SYNC_FAILED | Audit queue processing failed or is permanently blocked; ordinary retryable network failures only remain queued. |
INITIALIZATION_FAILED | Other UI or listener initialization failure. |
Versions and changes
This reference describes API v1 in release b0449f1, deployed on 2026-10-17. apiVersion identifies the integration contract; publicationVersion identifies a site configuration; consentEpoch identifies preference renewal. None is an immutable script URL. Additive API changes preserve v1; removing or changing the meaning of a method requires a new contract version.
2026-10-17 — API v1: readiness/status/error methods, three DOM events, immutable snapshots; existing methods retained. SPA recovery, explicit error phase and BiscottiInitializationError naming verified.
W16 — Developer mode. A separate key for localhost and staging tests. Test data stays out of production reports and billing.
Automatic updates (recommended): Paste the code once. Script fixes arrive automatically, without replacing the code manually. Existing installations and WordPress work the same way. Pinned version with SRI (advanced): You choose a specific script version. It receives no automatic fixes, including security fixes. To update it, you must replace the code manually. Published banner settings are fetched automatically in both modes.
W20 — SDK releases and SRI. /sdk/manifest.json lists the current and retained releases. src is an immutable URL, integrity its SHA-384 hash, and bytes its file size. Each release also has a manifest beside its script. Its URL contains the SHA-256 content hash and uses one-year immutable caching. Changing site configuration does not change the URL.
For a pinned SRI version: to upgrade, save the previous snippet and replace src and integrity together using the panel or manifest. Keep the site key and crossorigin="anonymous". Test readiness, denial, consent and withdrawal on localhost or staging first. To roll back, restore the previous src/integrity pair. Builds never overwrite or delete retained releases. Pinned installations require manual updates, including security fixes; never bypass an SRI failure by removing integrity.
A file hash is separate from apiVersion, publicationVersion and consentEpoch. Rolling back JavaScript does not roll back site publications or data. Retaining an old script does not promise indefinite compatibility with a future API; check its contract and behaviour before rollback.
W17 — biscotti:beforereload precedes required reload/replace after withdrawal. BeforeReloadDetail = SdkSnapshot + reason + navigation. Synchronous listener code on window runs before navigation. The event cannot cancel navigation; the SDK does not await asynchronous work. See the withdrawal section for ordering and an example.
{
"apiVersion": 1,
"version": "9c2bc03275827be7431adcc709a295afb1f7778553dbaf3c3c4f8a2df233f702",
"src": "/sdk/9c2bc03275827be7431adcc709a295afb1f7778553dbaf3c3c4f8a2df233f702/consent.js",
"integrity": "sha384-VtJRmbHcw1tU2ZG0b1SkNpDBvF0Wk2fuFyZG/CNH7stj+z9FgY8pbYK6eyk5KqmD",
"bytes": 83837
}