# JavaScript API The consent runtime exposes a flat API at `window.OptinStack`. Use it to read consent state, record decisions programmatically, subscribe to changes, and delegate UI helpers to the mounted banner plugin. ## Initialization [#initialization] Wait for the SDK to finish bootstrapping before calling API methods: ```js await window.OptinStack.ready; console.log(window.OptinStack.version); ``` `ready` is a Promise that resolves once the consent kernel, blocking, and plugins have initialized and the banner UI has been scheduled to load. It rejects if initialization fails. The banner plugin may register slightly after `ready` resolves; use `ui.*` helpers (which are safe no-ops until it mounts) or listen for `consent-updated` to react to user interaction. ## Consent state [#consent-state] ### `choices` [#choices] Returns the current consent choices as an object: ```js const choices = window.OptinStack.choices; // { necessary: true, analytics: false, marketing: false, preferences: false } ``` Assign a full object to update multiple categories at once. `necessary` is always forced to `true`. ```js window.OptinStack.choices = { necessary: true, analytics: true, marketing: false, preferences: false, }; ``` ### Category getters and setters [#category-getters-and-setters] Each category is available as a top-level property: ```js window.OptinStack.necessary; // always true (read-only) window.OptinStack.analytics; // boolean window.OptinStack.marketing; // boolean window.OptinStack.preferences; // boolean window.OptinStack.analytics = true; ``` ### `all` [#all] Write-only shorthand to accept or reject all optional categories: ```js window.OptinStack.all = true; // accept all window.OptinStack.all = false; // reject all optional categories ``` ### `acceptAll(source?)` and `rejectAll(source?)` [#acceptallsource-and-rejectallsource] ```js window.OptinStack.acceptAll('api'); window.OptinStack.rejectAll('api'); ``` The optional `source` argument is `'banner'`, `'preferences'`, or `'api'`. It is included in consent records and `consent-updated` events. ### `renew()` [#renew] Clears stored consent, resets choices to rule defaults, and sets `hasConsented` to `false`. The banner reappears when the user has not yet consented. ```js window.OptinStack.renew(); ``` ### `hasConsented` [#hasconsented] Whether the visitor has an active stored consent record: ```js if (window.OptinStack.hasConsented) { console.log('Returning visitor with saved consent'); } ``` Consent choices are also persisted in the first-party `optinstack` cookie. See [Consent Cookie](/consent-cookie) for the cookie format and subdomain sharing behavior. ## Context properties [#context-properties] | Property | Type | Description | | --------------- | --------- | ------------------------------------------------------------------------------------ | | `version` | `string` | SDK semver | | `gpc` | `boolean` | Global Privacy Control detected | | `region` | `string` | Detected ISO region code | | `mode` | `string` | Active consent model (`opt-in`, `opt-out`, or `informational`) | | `uiVariant` | `string` | Active banner display variant (`opt-in`, `opt-out`, `informational`, or `dont-sell`) | | `config.site` | `object` | Published site configuration applied by the runtime | | `config.policy` | `object` | Matched consent policy for the current visitor | ```js const mode = window.OptinStack.mode; const bannerType = window.OptinStack.uiVariant; const policy = window.OptinStack.config.policy; ``` `mode` is the consent **model** (how optional categories default and what counts as a decision). `uiVariant` is the banner **presentation**, which can be `dont-sell` while the underlying model remains `opt-out`. Prefer `uiVariant` when you need to know which UI the visitor sees. ## Server consent token [#server-consent-token] ### `getServerConsentToken()` [#getserverconsenttoken] Returns a short-lived signed token that your backend can verify with the [`@optinstack/server`](https://www.npmjs.com/package/@optinstack/server) package. Use it when a server route should only run after the visitor has granted specific categories. ```js await window.OptinStack.ready; const token = await window.OptinStack.getServerConsentToken(); await fetch('/api/personalize', { headers: { 'X-OptinStack-Consent': token, }, }); ``` Behavior: * Tokens are cached in the browser until near expiry, then refreshed automatically. * On **opt-in** policies, the call **throws** until the visitor has stored consent (`hasConsented === true`). On **opt-out** and **informational** policies, a default token may be available before an explicit interaction. * The token is meant for **your** backend verification. It is not a substitute for reading `OptinStack.choices` in the browser. Verify tokens in Node with `verifyConsentToken` and `isConsentAllowed` from `@optinstack/server`. See that package's README for install and server-side examples. ## `subscribe(callback)` [#subscribecallback] React to consent state changes. The first synchronous callback is skipped so you only receive updates after subscribing. ```js const unsubscribe = window.OptinStack.subscribe((state) => { console.log(state.choices, state.mode, state.gpc); }); // Later unsubscribe(); ``` ### Keyed subscribe [#keyed-subscribe] Subscribe to a single key: ```js window.OptinStack.subscribe('analytics', (value) => { console.log('Analytics consent:', value); }); window.OptinStack.subscribe('choices', (choices) => { console.log(choices); }); ``` Supported keys: `'choices'`, `'hasConsented'`, `'necessary'`, `'analytics'`, `'marketing'`, and `'preferences'`. ## `events.on()` and `events.off()` [#eventson-and-eventsoff] Subscribe to the typed event bus. See the [Events](/events) reference for all event names and payloads. ```js function onConsentUpdated(detail) { console.log(detail.choices, detail.source, detail.action); } window.OptinStack.events.on('consent-updated', onConsentUpdated); window.OptinStack.events.off('consent-updated', onConsentUpdated); ``` Wildcard listener: ```js window.OptinStack.events.on('*', (eventName, detail) => { console.log(eventName, detail); }); ``` Every bus event also fires as a DOM `CustomEvent` on `document.documentElement` with the `optinstack:` prefix (for example `optinstack:consent-updated`). ## `ui` [#ui] UI helpers delegated to the mounted banner plugin. These are no-ops until the banner bundle registers. ### `ui.setPlaceholderHTML(fn)` [#uisetplaceholderhtmlfn] Customize HTML shown inside blocked iframe placeholders when consent is denied: ```js window.OptinStack.ui.setPlaceholderHTML((categories) => { return '

Accept consent to view this content.

'; }); ``` ### `ui.renderDeclaration(target?, options?)` [#uirenderdeclarationtarget-options] Render the tracker declaration widget into host-page elements. Default target: `[data-optinstack-declaration]`. ```js window.OptinStack.ui.renderDeclaration(); window.OptinStack.ui.renderDeclaration('#my-declaration', { styles: '.custom { color: red; }', }); ``` Returns a cleanup function, or `undefined` if the banner plugin is not mounted yet. ## Advanced [#advanced] ### `registerUiPlugin(plugin)` [#registeruipluginplugin] Register a custom UI plugin. The production banner bundle calls this automatically. Advanced use only. ### `destroy()` [#destroy] Tears down blocking, plugins, UI, and event listeners. ```js window.OptinStack.destroy(); ``` After calling `destroy()` , the SDK cannot be restarted without reloading the page. # Banner Control The consent banner and preferences dialog are rendered by the banner UI plugin inside a Shadow DOM. There is no `ui.show()` or `ui.hide()` API. Use the consent API and DOM events below to interact with the banner programmatically. ## Open preferences [#open-preferences] Dispatch a DOM event that the banner plugin listens for: ```html Consent preferences ``` Wait for the SDK before dispatching if your script runs early: ```js await window.OptinStack.ready; window.dispatchEvent(new CustomEvent('optinstack:open-preferences', { bubbles: true })); ``` The event opens the preferences dialog for consent variants that include preferences. Informational banners do not have a preferences dialog, so the event is ignored for that variant. ## Reset consent and re-prompt [#reset-consent-and-re-prompt] Call `renew()` to clear stored consent and reset choices to rule defaults. When `hasConsented` becomes `false`, the banner appears again for visitors who had previously consented. ```js window.OptinStack.renew(); ``` Use this for "Change consent" links or consent expiry flows configured in the dashboard. ## Record consent from custom UI [#record-consent-from-custom-ui] Build your own accept/reject buttons and record decisions through the JavaScript API. The runtime handles script unblocking, persistence, consent record reporting, and Google Consent Mode updates automatically. ```js // Custom "Accept all" button document.getElementById('my-accept-btn').addEventListener('click', () => { window.OptinStack.acceptAll('api'); }); // Custom partial consent document.getElementById('my-analytics-btn').addEventListener('click', () => { window.OptinStack.choices = { necessary: true, analytics: true, marketing: false, preferences: false, }; }); ``` Pass `'api'` as the source when recording consent from your own UI so consent records reflect the origin correctly. ## Check consent before showing content [#check-consent-before-showing-content] Gate custom content on consent state: ```js await window.OptinStack.ready; function updateContent() { const showAnalytics = window.OptinStack.analytics; document.getElementById('analytics-section').hidden = !showAnalytics; } window.OptinStack.subscribe(updateContent); updateContent(); ``` ## Blocked iframe placeholders [#blocked-iframe-placeholders] When an iframe is blocked, the runtime shows a placeholder inside the iframe. Customize its HTML with `ui.setPlaceholderHTML()`: ```js window.OptinStack.ui.setPlaceholderHTML((categories) => { return ''; }); ``` Return trusted HTML only. If your placeholder includes dynamic text, escape it before building the string. From inside the placeholder (same origin as the iframe document), you can post a message to the parent page: ```js // Accept all consent from inside a blocked iframe placeholder parent.postMessage({ type: 'optinstack:action', action: 'acceptAll' }, '*'); // Open preferences from inside a blocked iframe placeholder parent.postMessage({ type: 'optinstack:action', action: 'openPreferences' }, '*'); ``` These messages are only accepted from OptinStack-controlled iframes. ## Tracker declaration widget [#tracker-declaration-widget] Render the tracker declaration list on your page (cookie / tracker inventory for the current project): ```html
``` ```js await window.OptinStack.ready; window.OptinStack.ui.renderDeclaration(); ``` Or target a specific element: ```js window.OptinStack.ui.renderDeclaration('#my-declaration', { styles: '.os-cookie-declaration { --os-decl-link-color: #047857; }', }); ``` The widget is filled from your **published tracker inventory** and providers, reflects the visitor's current choices, and can open preferences or withdraw optional consent when the banner UI supports it. If the banner plugin has not mounted yet, `renderDeclaration` returns `undefined`; wait for `ready` and retry or re-call after navigation. See the [JavaScript API](/api#ui) for full `renderDeclaration` options. ## Server-side consent checks [#server-side-consent-checks] To authorize backend work based on the same consent the runtime holds, request a short-lived token and verify it with `@optinstack/server`: ```js const token = await window.OptinStack.getServerConsentToken(); // Send token to your API (default header: X-OptinStack-Consent) ``` See [JavaScript API → Server consent token](/api#server-consent-token). # Configuration When the OptinStack script loads, it fetches your published project configuration and exposes the resolved result through the public `window.OptinStack` API. You do not configure the runtime by editing data on the page; you configure it in the dashboard, and the script applies it automatically. Configuration is managed in the OptinStack dashboard. The reference below describes the shape of what the runtime applies, useful when reading `window.OptinStack.config` for debugging or custom integrations. Published configuration is hostname-aware. Webflow, Framer, and custom sites use the same entitlement rules—platform choice never elevates features. Free can activate on each eligible hostname by default. A paid subscription attaches to one hostname and can move once to a registered, verified production hostname. Unassigned registered hostnames do not publish or serve runtime configuration. See [Domain Scans](/domain-scans) for details. ## `window.OptinStack.config` [#windowoptinstackconfig] After `await window.OptinStack.ready`, the resolved configuration is available read-only: | Property | Type | Description | | --------------- | -------- | ------------------------------------------------------------------------ | | `config.site` | `object` | Your published site config: policies, trackers, presentation, compliance | | `config.policy` | `object` | The consent policy matched for the current visitor's region | ### `config.site` sections [#configsite-sections] | Section | Description | | -------------- | ------------------------------------------------------------------------------------------------------------- | | `identity` | `siteId` (project ID) and `revision` (config version) | | `scope` | Page targeting (`pages: '*'` or path list) | | `policy` | Consent policies, categories, GPC signals, renewal settings | | `inventory` | Tracker list for blocking and preferences UI | | `presentation` | Theme, legal links, locale strings, interaction (scroll lock), vendor list | | `compliance` | Reserved compliance configuration. OptinStack is not currently an IAB TCF CMP and does not expose `__tcfapi`. | ### `scope.pages` (page targeting) [#scopepages-page-targeting] Control which paths show the banner and enforce consent UI: | Value | Behavior | | --------------- | -------------------------------------------------------------- | | `'*'` (default) | All pages | | Path list | Only matching pathnames (for example `['/shop', '/checkout']`) | Off-target pages still load the runtime for API access, but the banner is not shown for that visit. Configure targeting in the dashboard. ### `policy.policies[]` [#policypolicies] Each policy defines region-specific consent behavior: | Property | Type | Description | | --------------- | --------------------------------------------------------- | --------------------------------------------------------- | | `id` | `string` | Stable policy identifier (e.g. `'opt-in'`, `'dont-sell'`) | | `consentModel` | `'opt-in' \| 'opt-out' \| 'informational'` | Consent **model** (defaults and decision semantics) | | `uiVariant` | `'opt-in' \| 'opt-out' \| 'informational' \| 'dont-sell'` | Banner **UI** (includes Do Not Sell presentation) | | `scope.regions` | `string[]` | ISO region codes or `'global'` / `'eu'` | At runtime, `window.OptinStack.mode` reflects `consentModel` and `window.OptinStack.uiVariant` reflects `uiVariant`. ### `policy.renewal` [#policyrenewal] | Property | Type | Description | | ----------------------- | -------------------- | --------------------------------------- | | `promptOnExpiry` | `boolean` (optional) | Re-show the banner when consent expires | | `ttlDays` | `number` | Consent cookie expiration in days | | `shareAcrossSubdomains` | `boolean` | Share consent across subdomains | | `record.secure` | `boolean` | Require HTTPS for consent cookies | See [Consent Cookie](/consent-cookie) for the stored cookie format and how `shareAcrossSubdomains` affects subdomain access. ### `presentation.locale` and languages [#presentationlocale-and-languages] English is the fixed source language. You can enable secondary locales from the dashboard (23 languages total, including English). Locale strings power banner and preferences copy. The runtime resolves the visitor language from `html lang` (then falls back to English when a locale is not enabled). Language packs are generated on publish and loaded from the CDN, not as a separate script install. Need a language that is not listed? [Contact us](https://optinstack.com/contact) to request it. ### `presentation.interaction.scrollLock` [#presentationinteractionscrolllock] Optional background scroll lock while the banner or preferences UI is open (`both`, `banner`, `preferences`, or off depending on dashboard settings). Configure this in the dashboard; read it from `config.site.presentation` only for debugging. ### Vendors [#vendors] Vendor / provider definitions used by preferences and the tracker declaration appear under presentation (for example `presentation.vendors` / providers). They are managed from your tracker inventory and provider metadata in the dashboard. ## Policy matching [#policy-matching] The runtime evaluates policies in this priority order: 1. **Region match**: a policy whose `scope.regions` contains the visitor's detected region 2. **EU match**: if the visitor is in the EU and a policy includes `'eu'` 3. **Global fallback**: a policy with `'global'` in `scope.regions` Projects with region rules use the most specific matching policy available. Projects without region-specific rules fall back to the global policy. Regional rules and geo-targeted banner behavior require a plan that includes geo-targeting. Projects without that entitlement use the published global policy. ## When configuration takes effect [#when-configuration-takes-effect] Changes you make in the dashboard apply to new visits after you publish. Existing visitors keep their stored consent until it expires or they change it. # Consent Cookie OptinStack stores the visitor's current consent state in a first-party cookie named `optinstack`. The runtime reads this cookie during initialization to decide whether a visitor has already made a consent choice, which categories are granted, and whether the banner should appear again. ## Cookie format [#cookie-format] The cookie value is URL-encoded JSON. After decoding, it has this shape: ```json { "id": "f3f5cc3b-9c49-4f7b-9d30-4f5a5f6c2a10", "choices": { "necessary": true, "analytics": false, "marketing": false, "preferences": false }, "timestamp": 1760000000000, "configVersion": "site-config-version" } ``` | Field | Description | | --------------------- | ------------------------------------------------------------------ | | `id` | Consent record ID for the current saved choice. | | `choices.necessary` | Always `true`. Essential consent cannot be declined. | | `choices.analytics` | Whether analytics consent is granted. | | `choices.marketing` | Whether marketing consent is granted. | | `choices.preferences` | Whether preferences consent is granted. | | `timestamp` | Unix timestamp in milliseconds when this consent choice was saved. | | `configVersion` | Published configuration version that created the stored choice. | The cookie is written with `path=/` and expires based on the consent duration configured in the dashboard. ## Cross-subdomain sharing [#cross-subdomain-sharing] When cross-domain consent sharing is enabled for a project on Business or Enterprise, OptinStack writes the consent cookie for the registrable site domain. For example, a site configured for `example.com` can share the same `optinstack` cookie across: * `example.com` * `www.example.com` * `app.example.com` * `docs.example.com` That means a visitor who accepts consent on `www.example.com` can carry the same consent state to `app.example.com`, as long as both pages are under the same registrable domain and use the same OptinStack project configuration. For sharing to work reliably, every subdomain should install the same project snippet and be served over HTTPS when secure cookies are enabled. Sharing is not available for localhost, preview hosts on unrelated domains, or separate production domains. Browser cookies cannot be shared across unrelated domains, such as `example.com` and `example.net`. For that case, each domain needs its own consent state. Cross-subdomain consent sharing requires a Business or Enterprise plan. In OptinStack, cross-domain consent sharing means sharing across subdomains of the same site domain. It does not bypass browser restrictions for unrelated domains. Preview or staging hostnames from Webflow, Framer, and similar platforms usually live on a different registrable domain than your production site. They can be used to test banner behavior, but browser consent state will not carry from that preview hostname to your production hostname. ## Reading consent from a subdomain [#reading-consent-from-a-subdomain] If another page on a subdomain needs to check consent before the runtime API is available, it can read the `optinstack` cookie directly: ```js function readOptinStackConsent() { const value = document.cookie .split('; ') .find((part) => part.startsWith('optinstack=')) ?.split('=') .slice(1) .join('='); if (!value) return null; try { return JSON.parse(decodeURIComponent(value)); } catch { return null; } } const consent = readOptinStackConsent(); if (consent?.choices.analytics) { // Analytics consent is granted. } ``` When the OptinStack runtime is loaded, prefer the JavaScript API because it validates the stored consent, handles expiry and configuration changes, and keeps subscribers updated: ```js await window.OptinStack.ready; if (window.OptinStack.analytics) { // Analytics consent is granted. } ``` ## Renewal and configuration changes [#renewal-and-configuration-changes] The runtime may ignore or replace the stored cookie when: * The cookie has expired based on your configured consent duration. * Consent renewal is enabled and the saved choice is older than the allowed duration. * The published configuration version changed and visitors must be prompted again. * The visitor chooses to change consent through the banner, preferences dialog, or JavaScript API. Use `window.OptinStack.renew()` when you intentionally want to clear stored consent and re-prompt the visitor. # Consent forwarding Consent forwarding sends every saved consent record from OptinStack to an **HTTPS endpoint you control**. Use it for: * A customer-owned long-term archive (beyond OptinStack's standard 12-month retention) * Warehouse ingestion, custom reporting, or internal audit pipelines * A self-serve operational copy independent of [fair-use allowances](/fair-use-and-enterprise) OptinStack still stores each record normally. Forwarding runs in the background so a slow or failing endpoint **never blocks** the live Consent banner. Your endpoint receives consent choices and related identifiers. Prefer **Bearer token** authentication so only OptinStack (with your secret) can write to that API. **No auth** is available for quick tests, but unauthenticated public endpoints can be abused and are not recommended for production. ## Quick setup [#quick-setup] 1. Create a secure token (see below). 2. Build an HTTPS receiver that verifies `Authorization: Bearer `. 3. In the dashboard: **Settings → Consent → Records & archive**. 4. Enable **Consent forwarding**, enter the HTTPS URL, choose **Bearer token**, paste the token, then **Save**. 5. Click **Test endpoint** to send a synthetic consent event and confirm a 2xx response. Bearer tokens are **write-only** in OptinStack. After save, the dashboard only shows that a token is configured (not the secret). To rotate, paste a new token and save again. ## Generate a secure token [#generate-a-secure-token] Generate a secure shared secret using a **cryptographically secure random generator**. The secret should contain **at least 32 random bytes**. Store it only in your server environment and OptinStack settings—never in client-side code or git. Examples: **OpenSSL (recommended):** ```bash openssl rand -base64 32 ``` **Node.js:** ```bash node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))" ``` **Python:** ```bash python3 -c "import secrets; print(secrets.token_urlsafe(32))" ``` Example workflow: ```bash # 1. Generate at least 32 random bytes (encoded for pasting) TOKEN=$(openssl rand -base64 32) echo "$TOKEN" # paste into OptinStack Settings once # 2. Store on your receiver (never commit) export OPTINSTACK_FORWARDING_TOKEN="$TOKEN" ``` ## Request format [#request-format] OptinStack sends `POST` with JSON. ```http POST /api/optinstack/consents HTTP/1.1 Host: your-api.example.com Content-Type: application/json User-Agent: OptinStack-Consent-Forwarder/1.0 X-OptinStack-Project-Id: proj_123 X-OptinStack-Consent-Id: consent_123 Authorization: Bearer YOUR_TOKEN ``` | Header | Purpose | | ------------------------- | --------------------------------------------------------------------- | | `Authorization` | Present when you selected **Bearer token**. Format: `Bearer `. | | `X-OptinStack-Project-Id` | Project that produced the consent. | | `X-OptinStack-Consent-Id` | Consent record identifier. | | `User-Agent` | Always `OptinStack-Consent-Forwarder/1.0`. | The JSON body is the consent record, including flat category booleans such as `analytics`, `marketing`, and `preferences` (see product consent sync shape). Design your handler to accept additional fields over time. ## Verify the bearer token (example) [#verify-the-bearer-token-example] Always compare against a **server-side** secret. Reject missing or wrong tokens with `401`. ```ts import express from 'express'; const app = express(); app.use(express.json({ limit: '256kb' })); app.post('/api/optinstack/consents', (req, res) => { const expected = process.env.OPTINSTACK_FORWARDING_TOKEN; const auth = req.header('authorization') ?? ''; if (!expected || auth !== `Bearer ${expected}`) { return res.status(401).json({ error: 'Unauthorized' }); } const projectId = req.header('x-optinstack-project-id'); const consentId = req.header('x-optinstack-consent-id'); // Persist, enqueue, or warehouse-write here console.log('OptinStack consent', { projectId, consentId, action: req.body.action, analytics: req.body.analytics, marketing: req.body.marketing, preferences: req.body.preferences, }); // Prefer 2xx so OptinStack marks delivery successful return res.status(204).send(); }); app.listen(3000); ``` ### Checklist for production [#checklist-for-production] * [ ] HTTPS only (OptinStack requires a secure endpoint URL) * [ ] **Bearer token** enabled in OptinStack (not “No auth”) * [ ] Token generated with a CSPRNG (at least 32 random bytes; not a short password) * [ ] Token stored only in secrets manager / env on your receiver * [ ] Endpoint rejects requests without a valid `Authorization` header * [ ] **Test endpoint** succeeds from Settings after deploy * [ ] You monitor failures (Settings shows last failure reason when delivery fails) ## Delivery behavior [#delivery-behavior] * Project-level forwarding (Settings) is preferred. Older request-level `custom_endpoint` forwarding remains a fallback when no project endpoint is set. * Non-2xx responses or network errors are recorded on the project (last failure reason). Consent **ingest still succeeds** for the visitor. * Re-test after fixing your API; OptinStack does not re-send historical records automatically. ## Related [#related] * [Consent record retention](/consent-retention) — what OptinStack keeps for 12 months * [Fair-use and Enterprise](/fair-use-and-enterprise) — traffic allowances and custom volume # Google Consent Mode v2 OptinStack drives [Google Consent Mode v2](https://developers.google.com/tag-platform/security/guides/consent) automatically. On every consent state change, the runtime calls `gtag('consent', ...)` with the mapped signals and pushes OptinStack events to `dataLayer` for GTM triggers. ## How it works [#how-it-works] When the consent kernel initializes: 1. OptinStack ensures `window.dataLayer` exists (creates an empty array if missing). 2. OptinStack ensures `window.gtag` exists (installs a minimal function that pushes to `dataLayer` if missing). 3. The first Consent Mode call uses `gtag('consent', 'default', …)` and includes `wait_for_update: 500`. 4. Later consent changes use `gtag('consent', 'update', …)`. 5. DataLayer events such as `optinstack_consent_updated` are pushed for use in GTM triggers. You do **not** need to pre-install `gtag` for Consent Mode signals. You still need Google tags (or GTM) on the page for those tags to *consume* the signals. Place the OptinStack script **before** GTM / gtag so default consent is set early. See [Install](/install). Tracker **blocking** (scripts, iframes, cookies, storage) is separate from Consent Mode. Blocking uses your published inventory and `data-optinstack-categories`. Consent Mode only updates Google's consent signals. See [Basic vs Advanced Google Consent Mode](https://community.optinstack.com/articles/basic-vs-advanced-consent-mode) for when cookieless pings may still fire. ## Consent signal mapping [#consent-signal-mapping] | GTM parameter | OptinStack category | | ------------------------- | ------------------- | | `ad_personalization` | `marketing` | | `ad_storage` | `marketing` | | `ad_user_data` | `marketing` | | `analytics_storage` | `analytics` | | `functionality_storage` | `preferences` | | `personalization_storage` | `preferences` | | `security_storage` | `necessary` | Each parameter is set to `'granted'` or `'denied'` based on the corresponding OptinStack consent category, with the exceptions below. ## Disabled categories [#disabled-categories] Project configuration can disable entire categories (for example, hide the marketing toggle in consent preferences). When a category is **disabled** in config, the SDK treats it as essential for enforcement purposes and sends **`granted`** for the mapped Google Consent Mode signals on that category. Disabled categories are not shown in the preferences UI and cannot be declined by visitors. ## Preview and override modes [#preview-and-override-modes] Dashboard preview or rule-override flows can force a temporary banner rule in the browser. When an override forces an **opt-in** rule for preview, Consent Mode updates may be skipped so preview behavior does not fight production Google tags. Production traffic uses the published regional policy and always drives Consent Mode as described above. ## OptinStack GTM guides [#optinstack-gtm-guides] For full setup walkthroughs, see: * [Integrate OptinStack with Google Tag Manager](https://community.optinstack.com/articles/integrate-optinstack-with-google-tag-manager) * [Set up Google Consent Mode v2](https://community.optinstack.com/articles/set-up-google-consent-mode-v2) * [Basic vs Advanced Google Consent Mode](https://community.optinstack.com/articles/basic-vs-advanced-consent-mode) * [OptinStack and Google Tag Manager integration](https://optinstack.com/integrations/google-tag-manager) ## Example [#example] When a user accepts all categories, the SDK sends: ```js gtag('consent', 'update', { ad_personalization: 'granted', ad_storage: 'granted', ad_user_data: 'granted', analytics_storage: 'granted', functionality_storage: 'granted', personalization_storage: 'granted', security_storage: 'granted', }); ``` When a user rejects all optional categories: ```js gtag('consent', 'update', { ad_personalization: 'denied', ad_storage: 'denied', ad_user_data: 'denied', analytics_storage: 'denied', functionality_storage: 'denied', personalization_storage: 'denied', security_storage: 'granted', // necessary is always granted }); ``` ## DataLayer events [#datalayer-events] The SDK also pushes events to the `dataLayer` array for use in GTM triggers: | DataLayer event | When | | ---------------------------------- | --------------------------------------------------------------- | | `optinstack_gpc_signal_detected` | On init, if Global Privacy Control is active | | `optinstack_consent_updated` | Every time consent state changes | | `optinstack_necessary_accepted` | First time necessary is granted (fires once) | | `optinstack_analytics_accepted` | First time analytics is granted (fires once) | | `optinstack_marketing_accepted` | First time marketing is granted (fires once) | | `optinstack_preferences_accepted` | First time preferences is granted (fires once) | | `optinstack_unclassified_accepted` | First time all four consent categories are granted (fires once) | ### Using dataLayer events in GTM [#using-datalayer-events-in-gtm] Create a custom trigger in GTM based on these events: 1. Go to **Triggers** → **New** 2. Select **Custom Event** 3. Set the event name to `optinstack_analytics_accepted` 4. Use this trigger on tags that should only fire after the user grants analytics consent The `*_accepted` events fire only once per page load. They will not re-fire if consent is toggled off and on again within the same load. If a category is later denied, the corresponding accepted marker is cleared so a new grant can fire again later in the session. ## Webflow Insights [#webflow-insights] If your Webflow project uses Webflow Insights and follows an **opt-in** GDPR policy rule, configure Webflow Insights so that tracking is disabled until the user has granted consent. This ensures analytics are not collected before the user has explicitly opted in. Webflow Insights Inside Webflow designer, navigate to **Insights → Tracking** and set **Tracking defaults** to **Don't track by default**. The runtime also syncs analytics consent with Webflow's user-tracking APIs (`wf.allowUserTracking` / `wf.denyUserTracking`) when those APIs are present. # Consent record retention OptinStack retains consent records for **12 months on standard plans**. Retention is rolling: new consent records continue to be collected normally, while older records become eligible for automatic deletion after the retention window. ## What happens after 12 months [#what-happens-after-12-months] OptinStack uses a short technical buffer before deletion. After that buffer, records outside the retention window are removed automatically from OptinStack storage. Expired records cannot be recovered from OptinStack. If you need a long-term archive for audits, internal reporting, warehouse ingestion, or legal workflows, configure [Consent Forwarding](/consent-forwarding) so every consent event is also sent to an endpoint you control. ## What retention does not change [#what-retention-does-not-change] Retention does not disable your live Consent banner, tracker blocking, consent enforcement, or new consent collection. Visitors can continue submitting consent choices, and OptinStack continues storing new consent records within the active retention window. Consent Forwarding also continues when enabled, so your endpoint can keep its own copy independently of OptinStack's standard retention policy. ## Deleting records earlier [#deleting-records-earlier] You can delete individual consent records from the dashboard. Archiving a project stops runtime activity but retains its records. Verified project-wide erasure requires a support request. Enterprise customers can contact sales to discuss extended retention or custom storage terms under a separate agreement. See [Fair-use allowances and Enterprise traffic](/fair-use-and-enterprise) for traffic and custom-volume details. # Domain Scans OptinStack discovers trackers by loading pages on your registered hostname and recording scripts, storage, and related resources. Register the public hostname visitors actually use. If your site is served through a reverse proxy, that usually means the proxy hostname, not the private origin behind it. This page summarizes product behavior. The binding disclosure is published at [Domain scan disclosure](https://optinstack.com/legal/domain-scans) on our marketing site. ## Page limits by plan [#page-limits-by-plan] | Tier | Pages per scan | | ---------- | -------------- | | Free | 50 | | Pro | 100 | | Business | 350 | | Enterprise | 1,000 | These limits define how many discovered pages are included in each scan. Webflow, Framer, and custom hostnames use the same Free or paid assignment rules. Platform choice does not unlock paid features. Free scans are capped at 50 pages per run with a 24-hour cooldown by default. Free can activate on each eligible verified custom hostname or registered development hostname (unlimited Free hostnames by default). ## Which hostname should I register? [#which-hostname-should-i-register] Register the **exact** hostname visitors see in the browser address bar (after redirects): * Use `example.com` **or** `www.example.com` when that is the host that serves traffic—not both unless each is a separate production property you license. * Prefer the host that remains in the address bar after HTTPS redirects (if everyone lands on `www.`, register `www.`). * Use the custom production hostname when Webflow, Framer, or another platform serves your site on a connected domain. * Use the reverse-proxy hostname when traffic reaches your site through a proxy or edge layer. * Do not register a private origin hostname unless visitors also load the site directly from that hostname. ### Business rules [#business-rules] | Rule | Guidance | | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | Unit of coverage | One hostname = one Free assignment (unlimited Free hostnames by default), one paid Pro/Business subscription, or Workspace coverage | | Runtime identity | Banner config, consent, and assignment apply only to hostnames registered and entitled for the project | | www vs apex | Pick the host that serves visitors; redirect the other. They are not interchangeable unless both are registered and licensed | | Second public domain | Separate registration and license | | Private origin | Unsupported | OptinStack stores platform-detected hostnames and user-confirmed hostnames separately. Staging hostnames stay available for testing. Production scans, billing, consent records, and published runtime configuration use the configured public production hostname. ## Reverse proxies and custom hostnames [#reverse-proxies-and-custom-hostnames] Reverse proxies are supported when the public hostname is reachable over HTTPS and returns the pages visitors see. The scanner follows the public site, not your private origin infrastructure. There is no separate “origin hostname” mapping: the public host is the commercial and runtime identity. If `site.com` reverse proxies to `project.webflow.io`, register `site.com` as the production hostname. Keep the platform hostname as a staging or detected hostname for preview testing. Billing applies to the public production hostname, because that is the property where visitors use OptinStack. If you serve two different public domains, register and license each one. OptinStack does not treat cross-domain reverse-proxy pairs as a single licensed site. ## Framer and Webflow staging vs production [#framer-and-webflow-staging-vs-production] Embedded platforms often expose a staging hostname before a custom production domain exists. OptinStack treats these differently: * **Development hostname**: a Webflow or Framer hostname used before the production domain is ready. It follows the same Free or paid assignment rules as every other hostname. * **Free hostname**: a production-capable Free assignment on an eligible registered hostname (unlimited Free hostnames by default), with a 50-page scan cap and 24-hour cooldown by default. * **Platform production hostname**: detected from the platform and confirmable as your production hostname. * **Custom production hostname**: entered by you when visitors use a public domain that differs from the platform hostname, such as a reverse-proxy or connected domain. If you start setup with only a staging hostname, you can finish onboarding and test the banner. When a production hostname is added later, confirm it in OptinStack before using it for production scans, billing, and runtime configuration. ## Scanner access and WAFs [#scanner-access-and-wafs] The scanner needs to load your public pages over HTTPS. Results may be incomplete or fail when a firewall, bot protection rule, authentication wall, geo rule, or rate limit blocks automated browsing. If a scan fails on a reachable public site, allow **OptinStack-Bot** through your WAF or bot protection (see [OptinStack-Bot](https://optinstack.com/bot) for user agent and Web Bot Auth details), then run the scan again. OptinStack does not scan logged-in areas or private origins that are not publicly accessible. ## How URLs are chosen [#how-urls-are-chosen] 1. **Discover**: Read `https://{hostname}/sitemap.xml` when available (including child sitemaps when the root file is a sitemap index); otherwise crawl HTTPS links on `/`. 2. **Normalize**: Dedupe URLs (same path variants, bare root vs trailing slash). 3. **Order**: Pin homepage first when present; then common compliance paths (privacy, cookie policy, terms, contact, and similar when present); then stable locale-aware alphabetical order for the rest. 4. **Select**: Take the first *N* URLs for your plan (`N` = scan limit). Higher plans scan a longer prefix of the same ordered list. Free’s first 50 pages are always the first 50 pages a higher plan would scan for the same discovery result, when at least 50 URLs exist. ## Consistency [#consistency] For a **fixed** sitemap or homepage link set: * Repeated scans on the same plan should target the **same URLs**. * Activating a higher plan adds pages at the end of that ordered list; it does not reshuffle Free’s selection. ## When counts can still change [#when-counts-can-still-change] Even with the same URLs, tracker totals may vary because of: * Site or tag-manager changes between scans * Conditional loading (consent state, geography, experiments) * Timeouts or blocked third-party resources during automated loads * Pages not in the discovered set (login-only areas, unscanned URLs beyond your plan cap) ## Large sitemaps [#large-sitemaps] Sites with thousands of sitemap entries (for example multi-location brands) only scan up to the plan cap. Unscanned URLs are not represented in that run's tracker list. ## Sitemap indexes [#sitemap-indexes] If the root `sitemap.xml` is a **sitemap index**, we fetch same-host child sitemap documents and merge page URLs (subject to fetch and discovery caps). Flat urlsets list all page URLs in one file. ## What happens if my site has more pages than my plan? [#what-happens-if-my-site-has-more-pages-than-my-plan] We scan up to your plan limit from the ordered list above. The dashboard may show **scanned** vs **discovered** counts (for example, 50 / 2,775). Trackers on unscanned pages are not included until you upgrade, run again with a higher page limit, or add them manually. ## Will two scans pick the same pages? [#will-two-scans-pick-the-same-pages] Yes, when the discovered URL set is unchanged and you use the same plan. Changing your sitemap, homepage links, or plan can change which pages are selected. ## Why is my tracker count different between scans? [#why-is-my-tracker-count-different-between-scans] The same pages can still report different trackers when tags load conditionally, resources time out, or you changed scripts between runs. A different page set also changes totals. ## Related [#related] * [Domain scan disclosure](https://optinstack.com/legal/domain-scans), customer-facing legal FAQ # Events OptinStack emits events through two channels simultaneously: 1. **SDK events** via `OptinStack.events.on()` / `OptinStack.events.off()` 2. **DOM CustomEvents** on `document.documentElement` with the `optinstack:` prefix Both fire for every event. Use whichever fits your integration. SDK initialization is not an event. Await `window.OptinStack.ready` instead of listening for a `ready` event. ## Subscribing [#subscribing] ### SDK event API [#sdk-event-api] ```js await window.OptinStack.ready; function handler(detail) { console.log(detail); } window.OptinStack.events.on('consent-updated', handler); window.OptinStack.events.off('consent-updated', handler); ``` ### Wildcard listener [#wildcard-listener] ```js window.OptinStack.events.on('*', (eventName, detail) => { console.log('Event:', eventName, detail); }); ``` ### DOM CustomEvents [#dom-customevents] ```js document.documentElement.addEventListener('optinstack:consent-updated', (e) => { console.log('Consent changed:', e.detail.choices); }); ``` Events have `bubbles: true` so they propagate through the document tree. ## Consent events [#consent-events] ### `accept-all` [#accept-all] Fires immediately before all categories are set to `true`. ```js window.OptinStack.events.on('accept-all', () => { console.log('User accepted all categories'); }); ``` ### `reject-all` [#reject-all] Fires immediately before optional categories are set to `false`. ```js window.OptinStack.events.on('reject-all', () => { console.log('User rejected optional categories'); }); ``` ### `consent-updated` [#consent-updated] Fires after consent choices change. This is the primary event for reacting to consent state. ```js window.OptinStack.events.on('consent-updated', (detail) => { console.log(detail.id); // Consent record ID console.log(detail.choices); // { necessary, analytics, marketing, preferences } console.log(detail.previousChoices); // Previous state (if available) console.log(detail.action); // "accept_all" | "reject_all" | "submit" | "preferences" console.log(detail.source); // "banner" | "preferences" | "api" console.log(detail.timestamp); // Unix timestamp }); ``` This event only fires when choices actually change. Duplicate updates are deduplicated. ### `consent-cleared` [#consent-cleared] Fires after `renew()` clears stored consent and resets choices to the active rule defaults. Use this when you need to hide consent-gated UI until the visitor makes a new choice. ```js window.OptinStack.events.on('consent-cleared', (detail) => { console.log(detail.id); console.log(detail.choices); console.log(detail.timestamp); }); ``` ### `open-preferences` [#open-preferences] Fires when the preferences dialog should open. You can also dispatch `optinstack:open-preferences` on `window` from page code. See [Banner control](/banner-control). ## Blocking events [#blocking-events] These events fire when the runtime blocks or unblocks third-party resources based on consent. ### `script-blocked` / `script-unblocked` [#script-blocked--script-unblocked] ```js window.OptinStack.events.on('script-blocked', (detail) => { console.log(detail.src); console.log(detail.element); // HTMLScriptElement console.log(detail.categories); // ["analytics", "marketing"] }); ``` ### `iframe-blocked` / `iframe-unblocked` [#iframe-blocked--iframe-unblocked] ```js window.OptinStack.events.on('iframe-blocked', (detail) => { console.log(detail.src); console.log(detail.element); // HTMLIFrameElement console.log(detail.categories); }); ``` ### `map-blocked` / `map-unblocked` [#map-blocked--map-unblocked] Specific to Webflow map embeds: ```js window.OptinStack.events.on('map-blocked', (detail) => { console.log(detail.element); console.log(detail.categories); }); ``` ### `storage-blocked` [#storage-blocked] Fires when access to `localStorage` or `sessionStorage` is blocked for a key that requires consent: ```js window.OptinStack.events.on('storage-blocked', (detail) => { console.log(detail.key); console.log(detail.storageType); // "localStorage" | "sessionStorage" console.log(detail.categories); }); ``` ### `cookie-blocked` [#cookie-blocked] Fires when a tracker is blocked or purged: ```js window.OptinStack.events.on('cookie-blocked', (detail) => { console.log(detail.name); console.log(detail.categories); console.log(detail.action); // "blocked" | "purged" }); ``` ## Reporting events [#reporting-events] ### `consent-sync-failed` [#consent-sync-failed] Fires when the runtime cannot record a consent update: ```js window.OptinStack.events.on('consent-sync-failed', (detail) => { console.error(detail.error, detail.payload); }); ``` ## Event reference [#event-reference] | Event | Payload | Description | | --------------------- | ---------------------------------------------------------------- | -------------------------------------------- | | `accept-all` | : | All categories accepted | | `reject-all` | : | Optional categories rejected | | `consent-updated` | `{ id, choices, previousChoices?, action?, source?, timestamp }` | Consent state changed | | `consent-cleared` | `{ id, choices, timestamp }` | Stored consent cleared and defaults restored | | `open-preferences` | : | Preferences dialog should open | | `script-blocked` | `{ src, element, categories }` | Script blocked | | `script-unblocked` | `{ src, element, categories }` | Script unblocked | | `iframe-blocked` | `{ src, element, categories }` | Iframe blocked | | `iframe-unblocked` | `{ src, element, categories }` | Iframe unblocked | | `map-blocked` | `{ element, categories }` | Map embed blocked | | `map-unblocked` | `{ element, categories }` | Map embed unblocked | | `storage-blocked` | `{ key, storageType, categories }` | Storage access blocked | | `cookie-blocked` | `{ name, categories, action }` | Tracker blocked or purged | | `consent-sync-failed` | `{ error, payload }` | Consent record could not be reported | # Fair-use allowances and Enterprise traffic OptinStack standard plans include monthly fair-use allowances for consent events and runtime requests. These allowances help keep infrastructure sustainable while allowing every project to keep its live Consent banner running. | Plan | Monthly consent events | Monthly runtime requests | | ---------- | ---------------------: | -----------------------: | | Pro | 1,000,000 | 10,000,000 | | Business | 2,000,000 | 25,000,000 | | Enterprise | Custom | Custom | ## What counts toward usage [#what-counts-toward-usage] A consent event is created when a visitor submits or updates consent choices. Runtime requests are requests made by the installed OptinStack runtime while loading configuration, syncing consent, or serving the live banner experience. Usage is measured monthly per project. If your site has seasonal spikes, marketing campaigns, or sudden press traffic, your usage may temporarily rise above normal. ## What happens near or over allowance [#what-happens-near-or-over-allowance] When usage approaches or exceeds a plan allowance, OptinStack may show in-product usage status and send email notices to the project owner. These notices are meant to help you plan ahead. OptinStack does not automatically disable the live Consent banner, tracker blocking, consent collection, analytics access, or Consent Forwarding just because a project goes over its monthly fair-use allowance. If high usage is sustained, we may recommend upgrading or moving to an Enterprise agreement with custom traffic allowances. ## Enterprise options [#enterprise-options] Enterprise plans can include: * Higher monthly consent-event and runtime-request allowances. * Custom commercial terms for very high-traffic properties. * Extended consent retention under a written agreement. * Support review for unusually high retained-storage or analytics volume. ## Retention and forwarding [#retention-and-forwarding] Fair-use allowances are separate from consent record retention. Standard-plan consent records are retained by OptinStack for [12 months](/consent-retention). If you need a long-term archive, configure [Consent Forwarding](/consent-forwarding) so every consent event is also sent to an endpoint you control. # Getting Started ## Installation [#installation] Add the OptinStack script tag to your page. It loads the consent runtime, which fetches your project configuration and renders the banner automatically: ```html ``` Replace `{projectId}` with your project ID from the OptinStack dashboard. Place the tag as the first element inside ``, before any third-party scripts. ## How it works [#how-it-works] When the script loads, the runtime: 1. Loads the consent runtime bundle and fetches your published project configuration using the `data-optinstack-site` attribute 2. Detects the visitor's region and matches the appropriate consent rule 3. Checks for existing consent stored in the browser 4. Renders the consent banner inside a Shadow DOM (isolated from your page styles) 5. Blocks known third-party scripts and iframes from your published tracker inventory, plus resources you manually tag with consent categories 6. Records consent updates so they are available in OptinStack reporting The runtime resolves configuration for the current hostname only when that hostname has Free, paid, Enterprise, or Workspace coverage. For production, use the public hostname visitors see. Webflow and Framer development hostnames can carry Free or paid coverage while you build; Free can also activate on the verified production hostname, and paid plans include one self-service move. See [Domain Scans](/domain-scans) for hostname, reverse-proxy, and scanner access guidance. ## Using the API [#using-the-api] Wait for initialization before calling API methods: ```js await window.OptinStack.ready; console.log(window.OptinStack.version); console.log(window.OptinStack.choices); ``` If your code runs in a module or after the script tag, `await OptinStack.ready` is sufficient. For inline scripts that may run before the bundle finishes loading, defer your logic: ```js (async () => { await window.OptinStack.ready; window.OptinStack.events.on('consent-updated', (detail) => { console.log('Consent changed:', detail.choices); }); })(); ``` See the [JavaScript API](/api) reference for the full surface. ## Consent categories [#consent-categories] OptinStack uses four fixed consent categories: | Category | Default | Description | | ------------- | -------------- | -------------------------------------- | | `necessary` | Always `true` | Essential trackers, cannot be declined | | `analytics` | Varies by mode | Analytics and performance tracking | | `marketing` | Varies by mode | Advertising and marketing trackers | | `preferences` | Varies by mode | Functionality and personalization | The `necessary` category is always `true` and cannot be set to `false`. This is enforced at every level of the SDK. ## Consent model vs banner variant [#consent-model-vs-banner-variant] Two related values describe the matched regional policy: | Property | Values | Meaning | | ----------------------- | ------------------------------------------------- | ------------------------------------------------------------- | | `mode` (`consentModel`) | `opt-in`, `opt-out`, `informational` | How optional categories default and what counts as a decision | | `uiVariant` | `opt-in`, `opt-out`, `informational`, `dont-sell` | Which banner UI the visitor sees | | Model / variant | Behavior | | -------------------------------- | --------------------------------------------------------------------------------- | | `opt-in` | Optional categories default to `false`. The visitor must explicitly accept. | | `opt-out` | Optional categories default to `true`. The visitor can reject. | | `informational` | Banner acknowledges notice and records accept-all consent. | | `dont-sell` (**uiVariant** only) | Banner uses "Do Not Sell or Share" style copy; the underlying model is `opt-out`. | ```js await window.OptinStack.ready; console.log(window.OptinStack.mode); // consent model console.log(window.OptinStack.uiVariant); // banner UI ``` ## Languages [#languages] English is the fixed source language. Free and Pro publish English only. On Business and Enterprise, you can enable secondary locales (22 languages beyond English, including major EU, APAC, and MENA languages). The runtime loads published packs from the CDN using the page `html lang` (normalized to a primary subtag such as `fr` from `fr-CA`). Languages you have not enabled fall back to English. Translations update when you publish configuration; they do not require reinstalling the script. If you need a language that is not listed yet, [contact us](https://optinstack.com/contact) and we will review adding it. See banner language settings in the [dashboard](https://app.optinstack.com). ## Global Privacy Control [#global-privacy-control] When the browser sends a [Global Privacy Control](https://globalprivacycontrol.org/) signal (`navigator.globalPrivacyControl === true`), the runtime records `gpc: true`. By default, all optional category defaults become `false`. On plans with advanced GPC controls, you can configure which optional categories GPC rejects. ```js await window.OptinStack.ready; if (window.OptinStack.gpc) { console.log('GPC signal detected, optional categories defaulted to false'); } ``` GPC is also reflected in consent records and in the GTM dataLayer event `optinstack_gpc_signal_detected`. See [Google Consent Mode](/consent-mode). ## Next steps [#next-steps] Add the runtime script and place it before third-party trackers. Choose the right public hostname for scans, staging previews, and reverse proxies. Read and write consent state, subscribe to changes, and use UI helpers. Read stored consent state and understand subdomain sharing. Open preferences, reset consent, and build custom consent flows. Subscribe to consent, blocking, and reporting events. # Welcome OptinStack is a Consent Management Platform (CMP) runtime for websites. It renders a consent banner and preferences dialog inside a Shadow DOM, manages visitor consent state, blocks and unblocks third-party scripts and iframes, and records consent updates for reporting. This site documents the browser runtime, JavaScript API, consent events, and integration patterns for developers adding OptinStack to a site. Product setup, compliance guides, and platform walkthroughs live on [optinstack.com](https://optinstack.com). ## Key topics [#key-topics] Add the OptinStack runtime script to your site. Read and update consent state through `window.OptinStack`. Open preferences, reset consent, custom flows, and iframe placeholder messaging. Subscribe to consent changes, blocking events, and reporting errors. Understand stored consent state and sharing across subdomains. How OptinStack drives gtag Consent Mode v2 and dataLayer events. ## Quick start [#quick-start] Add the OptinStack script to your page. It loads the consent runtime, which fetches your project configuration and renders the banner: ```html ``` Once loaded, the API is available at `window.OptinStack`: ```js await window.OptinStack.ready; console.log(window.OptinStack.choices); ``` Replace `{projectId}` with your project ID from the dashboard. See [Getting started](/getting-started) for installation details. # Install OptinStack is added to your site with a single ` ``` Do not add `async` or `defer` to the OptinStack tag. The browser pauses HTML parsing while a plain script executes, which gives the runtime its earliest chance to load configuration and install blocking before later third-party tags run. ## Attributes [#attributes] | Attribute | Required | Description | | ---------------------- | -------- | -------------------------------------------------------------------------------------- | | `src` | Yes | Runtime URL. Use `https://api.optinstack.com/v1/js/runtime/stable/optinstack.js`. | | `data-optinstack-site` | Yes | Your project ID from the dashboard. Links the install to your published configuration. | Replace `{projectId}` with the value from your dashboard. The dashboard snippet always contains the correct value. ## Pinned runtime with SRI [#pinned-runtime-with-sri] Every project can use a pinned runtime version with Subresource Integrity. Pinned installs trade automatic runtime hotfixes for a reviewable, immutable script URL and a browser-enforced integrity hash. ```html ``` Banner configuration, tracker inventory, region context, and translations continue to load as JSON data, so publishing banner copy or translations does not change the pinned runtime bytes. To receive runtime code updates, select a newer pinned version in the dashboard. ## Where the script goes [#where-the-script-goes] Add the tag as the **first child** of ``, before any third-party scripts or iframes you want consent to govern: ```html title="index.html" ``` Placing it first lets OptinStack block known trackers from your published inventory, plus resources you manually tag with `data-optinstack-categories`, before those resources load. Unknown untagged resources are not universally blocked until they are scanned, classified, manually added, or tagged. If a tag manager or platform injects trackers above OptinStack, those resources can run before consent enforcement is available. ## Content Security Policy (CSP) [#content-security-policy-csp] If your site uses a strict CSP, allow OptinStack to load the runtime and fetch configuration from the OptinStack API origin used in your install (production: `https://api.optinstack.com`). Typical requirements: | Directive | Why | | -------------------------- | ------------------------------------------------------------------------------------------------ | | `script-src` | Load `optinstack.js` (and `banner.js` when the runtime loads the UI bundle) | | `connect-src` | Fetch published configuration, record consent, and related runtime API calls | | `style-src` / font sources | Banner UI may inject styles and load fonts from the API origin | | `frame-src` / `child-src` | Only if you embed OptinStack-related iframes or blocked-iframe placeholders interact with frames | Exact values depend on your CSP style (`'self'`, nonces, hashes). Prefer allowing the documented API host rather than open wildcards. After changing CSP, verify the banner appears, consent records, and Google tags still receive Consent Mode updates. ## Manual category tagging [#manual-category-tagging] Use `data-optinstack-categories` when you need to govern a custom script or iframe before it appears in your tracker inventory. The value is a comma-separated list of consent categories. The runtime blocks the resource until every listed category is granted. ```html ``` Supported optional categories are `analytics`, `marketing`, and `preferences`. The `necessary` category is always allowed and should only be used for resources that are essential to the site. ## Next steps [#next-steps] Verify your install and read consent state with the JavaScript API. The full `window.OptinStack` surface: choices, events, and UI helpers. Platform-specific setup for Webflow, Framer, Next.js, and more. Understand production and development hostnames, reverse proxies, and scan limits. # Integrations OptinStack loads the consent runtime from a single script tag. The runtime fetches your project configuration and renders the banner UI. Every integration uses the same browser API documented in [JavaScript API](/api) and [Banner control](/banner-control). ## Script tag [#script-tag] The universal install method for any website: ```html ``` Replace `{projectId}` with your project ID from the dashboard. Place the tag as the first element inside ``, before any third-party trackers you want blocked. See [Install](/install) for placement details. After load, use the API: ```js await window.OptinStack.ready; window.OptinStack.events.on('consent-updated', (detail) => { console.log(detail.choices); }); ``` Platform-specific setup guides with screenshots and dashboard steps are on the marketing site: * [Script tag](https://optinstack.com/integrations/script-tag) * [Google Tag Manager](https://optinstack.com/integrations/google-tag-manager) ## Webflow [#webflow] Install the OptinStack Webflow app to configure and preview consent in Designer, then add the OptinStack runtime snippet as the first script in your site ``. Publishing the app configuration does not inject the runtime automatically. * [Webflow integration guide](https://optinstack.com/integrations/webflow) The runtime includes Webflow-specific blocking for map embeds and integrates with Webflow Analyze consent APIs when analytics consent changes. Register the Webflow development hostname and activate Free or a paid plan for live runtime testing. Confirm the public production hostname visitors use before launch, especially if a custom domain or reverse proxy sits in front of Webflow. Free can also activate on the production host; paid plans include one self-service move. See [Domain Scans](/domain-scans) for details. ## Framer [#framer] Install the OptinStack Framer plugin to configure and preview consent in your canvas, then add the OptinStack runtime snippet as early as possible in your published site ``. The plugin does not embed the runtime automatically. * [Framer integration guide](https://optinstack.com/integrations/framer) Register the Framer development hostname and activate Free or a paid plan for live runtime testing. Confirm the public production hostname visitors use before launch, especially if a custom domain or reverse proxy sits in front of Framer. Free can also activate on the production host; paid plans include one self-service move. See [Domain Scans](/domain-scans) for details. ## Web [#web] Load the OptinStack runtime as early as possible in your document ``, before any third party scripts that may set cookies or access storage. ```html ``` The OptinStack runtime should be the first third party script in the document so it can evaluate consent before any other resources load. Place analytics, advertising, chat, embedded widgets, and other third party scripts after it. For custom resources that have not yet appeared in your tracker inventory, use the `data-optinstack-categories` attribute to assign the appropriate consent category. ## Dashboard [#dashboard] Configure projects, consent rules, tracker blocking, and banner design in the OptinStack dashboard: * [app.optinstack.com](https://app.optinstack.com) The dashboard generates your project ID and install snippet. Copy the script tag from the Implementation step in your project settings. ## All integrations [#all-integrations] Browse the full integration catalog on the marketing site: * [optinstack.com/integrations](https://optinstack.com/integrations) ## Programmatic control [#programmatic-control] Regardless of platform, you interact with the banner and consent state through the same API: | Goal | Approach | | ------------------- | ------------------------------------------------------------ | | Read consent | `OptinStack.choices` or category getters | | Accept / reject | `OptinStack.acceptAll()` / `rejectAll()` or category setters | | Open preferences | Dispatch `optinstack:open-preferences` on `window` | | Reset and re-prompt | `OptinStack.renew()` | | React to changes | `OptinStack.subscribe()` or `events.on('consent-updated')` | See [Banner control](/banner-control) for examples. # Preferred consent storage region # Preferred consent storage region [#preferred-consent-storage-region] Business and Enterprise projects, including eligible Workspace Business projects, can confirm one preferred region before their first production publish. Free, Lite, and Pro projects use Western Europe by default. The available Cloudflare R2 location hints are Western Europe, Eastern Europe, Western North America, Eastern North America, Asia-Pacific, and Oceania. The preference covers: * authoritative consent records; * regional cold consent-analytics archives; and * temporary CSV export artifacts. It does not cover Analytics Engine, edge or transient processing, queues, Pipelines, service logs, support, security, or administration. Analytics Engine remains a global hot-analytics and fair-use system with a maximum three-month window. A Cloudflare location hint is best-effort. It is not a country-specific storage, data residency, jurisdiction, or exclusive-access guarantee. The customer choice can be made once. Downgrading and upgrading later does not reset it. If a live Free, Lite, or Pro project upgrades to Business, its selection affects only future records. Existing records stay in the regions assigned when they were created; authorized reads, exports, retention, and deletion include every historical assignment. Automated historical migration is not available. Contact support if you need to discuss an exceptional migration.