Consent

Let your consent management platform hold, withdraw, and reapprove Browser SDK collection.

Consent control is optional. Ordinary installations start automatically and keep their page-memory fallback when storage is unavailable. If your deployment requires consent, your consent management platform (CMP) decides whether the selected vendor and purposes are approved. Switchfrog enforces the decision you apply. It does not provide a banner, choose a consent category or legal basis, or keep your evidence of consent.

Start held

Install the held SDK before any other integration can start it:

<script
  src="https://api.switchfrog.com/sdk/v1.js"
  data-publishable-key="sf_pk_your_publishable_key"
  data-wait-for-consent
></script>

A bare data-wait-for-consent is equivalent to data-wait-for-consent="true". An absent attribute leaves the option omitted; "false" explicitly selects ordinary startup. Other values, including "TRUE", are invalid and prevent automatic startup. Attributes configure initialization only. Changing the DOM attribute later does not change permission.

With manual initialization, pass waitForConsent: true:

const client = window.Switchfrog.init("sf_pk_your_publishable_key", {
  waitForConsent: true,
})

init() reads the scoped saved collection preference even when waitForConsent is omitted, so it can honor an earlier opt-out. Initialization alone writes no consent preference or tracking state. Later permitted runtime operations can use session/tracking storage. start() and identify() start behavioral event listeners. Token access does not, but permitted session initialization can enable configured requests or schema observation. A normal keyed hosted tag requests startup automatically.

The initial wait writes no tracking state, restores no session, creates no field matcher or per-context matching key, and makes no SDK runtime requests. Fetching the hosted script itself is still a request. If that download also requires permission, delay loading it through your CMP and retain a continuing withdrawal bridge after it loads. Preserve your Content Security Policy.

Consent is independent of the project's behavior, requests, or schema capture level, its Actions entitlement, and server admission. When schema capture is permitted, the SDK may inspect eligible scalar values temporarily in bounded browser memory to link field locations. Field-link evidence contains field locations and occurrence references, not copied scalar values, matching keys, or digests. Request paths, origins, query names, and property names remain customer data. linkExclusions affects local matching; it does not redact transmitted paths or structural metadata.

Withdrawal discards the local matcher, its scoped key, pending observations, and frozen retries through the same local cleanup boundary. Reapproval starts with fresh matching ownership and cannot adopt pre-withdrawal work.

Remove any ordinary automatic installer for the same Site and endpoint before using this setup. A late held initialization cannot undo collection that already started. Use current SDK and integration versions everywhere, then reload. A new loader rejects an incompatible older runtime; it cannot recall work the older runtime already sent. Arbitrary mixed-version startup and withdrawal are unsupported.

Apply the current decision

Read the CMP's saved or current decision on every full page load. Unknown consent leaves the initial wait untouched. Approval calls client.optIn(), which grants permission and starts collection itself. Refusal or withdrawal calls client.optOut(). Neither start(), identify(), reset(), nor token access can regrant permission.

Use one page-owned bridge. The following file loads the held SDK itself, so use it instead of the script tag above. It keeps only the latest CMP decision while loading, including a withdrawal that supersedes an earlier approval. Place exactly one provider recipe from the following sections at the marked location inside the function.

// /public/switchfrog-consent.js (served as /switchfrog-consent.js)
;(() => {
  if (window.switchfrogConsent) return

  let client
  let decision

  function reportError(error) {
    if (error?.name !== "AbortError") {
      console.error("Switchfrog consent operation failed", error)
    }
  }

  function applyConsent(allowed) {
    if (typeof allowed !== "boolean") throw new TypeError("Expected a CMP boolean")
    decision = allowed
    if (client) {
      void (allowed ? client.optIn() : client.optOut()).catch(reportError)
    }
  }

  const script = document.createElement("script")
  script.src = "https://api.switchfrog.com/sdk/v1.js"
  script.setAttribute("data-publishable-key", "sf_pk_your_publishable_key")
  script.setAttribute("data-wait-for-consent", "true")
  const nonce = document.currentScript?.nonce
  if (nonce) script.nonce = nonce

  const ready = new Promise((resolve, reject) => {
    script.onload = () => {
      try {
        client = window.Switchfrog.init("sf_pk_your_publishable_key", {
          waitForConsent: true,
        })
        if (decision !== undefined) applyConsent(decision)
        resolve(client)
      } catch (error) {
        reject(error)
      }
    }
    script.onerror = () => reject(new Error("Switchfrog script did not load"))
  })
  window.switchfrogConsent = { applyConsent, ready }
  void ready.catch(reportError)

  // Install one CMP recipe here, before appending the SDK script.

  document.head.append(script)
})()

window.switchfrogConsent is application-owned example code, not a Switchfrog API. ready means the held client is available, not that permission is granted or session startup has finished. Keep this bridge alive across SPA route changes. Do not release permission from a component mount effect or equate component unmount with withdrawal.

Sourcepoint

Use vendor-scoped Custom JS Consent and Reject Actions. Configure Switchfrog and its required purposes with consent as their legal basis for this recipe. A Consent Action can also run for legitimate interest, so its name alone does not establish consent.

Leave On status change only unchecked for both actions so saved decisions run on later pages. Leave When new user unchecked for Reject Actions. Match geographic scope to your policy. AMP does not support these actions. See Sourcepoint's Consent Actions and Reject Actions.

Insert this recipe in the bridge, and load that bridge before Sourcepoint can execute either action:

window.applySwitchfrogSourcepointConsent = applyConsent

Set the vendor's Consent Action to:

window.applySwitchfrogSourcepointConsent(true)

Set its Reject Action to:

window.applySwitchfrogSourcepointConsent(false)

This order permits either action before or after the SDK download finishes. Test both actions against the actual vendor and required purposes. Do not substitute a banner-close or generic readiness event.

If you already use Sourcepoint snapshots, TCF exposes __tcfapi("getCustomVendorConsents", 2, callback); GDPR Standard exposes _sp_.gdpr.getCustomVendorConsents(callback). Check the configured vendor and every required purpose. Register readiness through _sp_.config.events. Invalidate delayed snapshots when a newer decision arrives, and withdraw synchronously rather than waiting for a snapshot request. See the TCF API, Standard API, and event callbacks.

OneTrust

Replace YOUR_ONETRUST_GROUP_ID with your actual consent-required group. An Always Active group is unsuitable for this recipe; C0002 has no universal meaning.

const groupId = "YOUR_ONETRUST_GROUP_ID"
let oneTrustResolved = false

function syncOneTrust() {
  const groups = window.OnetrustActiveGroups
  const cmp = window.OneTrust
  if (typeof groups !== "string" || typeof cmp?.IsAlertBoxClosed !== "function") return
  const validDecision = cmp.IsAlertBoxClosed()
  if (!validDecision && !oneTrustResolved) return
  oneTrustResolved = true
  applyConsent(validDecision && groups.split(",").includes(groupId))
}

window.addEventListener("OneTrustGroupsUpdated", syncOneTrust)
syncOneTrust()

The immediate read covers an already-loaded CMP; the listener covers later loading and changes. Use denied defaults for explicit consent. IsAlertBoxClosed() checks decision validity, not group approval. Before any valid decision, this recipe stays held. Once a decision has been applied, loss of validity withdraws permission. Exact group membership avoids matching another group's ID. Keep the site's existing OptanonWrapper. See OneTrust's JavaScript methods and events.

Cookiebot

This example uses the site's chosen statistics category. It does not classify all Switchfrog deployments as statistics.

let cookiebotResolved = false

function syncCookiebot() {
  const cmp = window.Cookiebot
  if (!cmp || (!cmp.hasResponse && !cookiebotResolved)) return
  cookiebotResolved = true
  applyConsent(cmp.hasResponse && cmp.consent.statistics === true)
}

window.addEventListener("CookiebotOnConsentReady", syncCookiebot)
window.addEventListener("CookiebotOnAccept", syncCookiebot)
window.addEventListener("CookiebotOnDecline", () => {
  cookiebotResolved = true
  applyConsent(false)
})
syncCookiebot()

CookiebotOnConsentReady covers saved and new choices. hasResponse distinguishes a decision from the initial state. An accept event still checks the selected category; partial approval can deny this deployment. Repeated callbacks are safe. See the Cookiebot developer API.

If your application has its own withdrawal button, call window.switchfrogConsent.applyConsent(false) synchronously before Cookiebot.withdraw() in that same handler. Do not assume an undocumented event covers custom withdrawal paths.

Reconnect current identity

Withdrawal discards cached SDK identity. Static data-user-id and data-account-id attributes, including Ghost member attributes, are startup-only and are not replayed after reapproval. Use the application's existing auth synchronizer to read current identity after every grant. An identity captured before withdrawal, or returned by an invalidated async auth read, must not be reused.

The Better Auth integration accepts waitForConsent: true and performs this resynchronization internally. Do not add another identity synchronizer beside it.

For other auth owners, connect onConsentChange() to the existing synchronizer as shown below. The example's authSync is your application's auth owner: invalidate() cancels pending reads and advances its auth revision; refresh() reads the current authenticated session, checks that revision after every await, and only then identifies. The same owner must invalidate on logout and user/account changes, apply the normal reset boundary, and handle errors. Neither function may call optIn().

React and Next.js

In a React SPA, load /switchfrog-consent.js once in the HTML document before the application entry and before Sourcepoint actions. In Next.js App Router, put it in the root layout with beforeInteractive. Browser globals stay inside the external file, so server rendering does not execute them:

// app/layout.jsx
import Script from "next/script"

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <Script src="/switchfrog-consent.js" strategy="beforeInteractive" />
        {children}
      </body>
    </html>
  )
}

Keep your CMP loader after the bridge when using Sourcepoint actions. Do not add onLoad to a beforeInteractive script; this strategy does not support it. The bridge's own ready promise handles the SDK load. See the Next.js Script reference.

Wire identity once in the root client component containing your existing auth synchronizer:

"use client"
import { useEffect } from "react"

export function ConsentIdentity({ authSync }) {
  useEffect(() => {
    let disposed = false
    let unsubscribe
    void window.switchfrogConsent.ready.then((client) => {
      if (disposed) return
      unsubscribe = client.onConsentChange((allowed) => {
        authSync.invalidate()
        if (allowed) authSync.refresh()
      })
    }).catch(authSync.reportError)
    return () => {
      disposed = true
      unsubscribe?.()
      authSync.invalidate()
    }
  }, [authSync])
  return null
}

Keep authSync stable and mount this component once at the auth owner's root. Cleanup cancels the identity subscription and stale reads, while the page-owned CMP bridge remains installed. This also handles React's extra development setup/cleanup cycle in Strict Mode.

Vue and Nuxt

For a Vue SPA, place the bridge before the application entry in index.html:

<script src="/switchfrog-consent.js"></script>
<script type="module" src="/src/main.js"></script>

In Nuxt, serve the file from public/ and add the same classic script to the document head:

// nuxt.config.ts
export default defineNuxtConfig({
  app: {
    head: { script: [{ src: "/switchfrog-consent.js" }] },
  },
})

Keep Sourcepoint's loader after that head script. Use a client-only app/plugins/consent-identity.client.js plugin to provide an existing auth owner if needed; Nuxt's plugin conventions keep .client plugins out of server execution. Do not install a second page bridge in the plugin.

In the persistent root auth component, the same subscription has an explicit cleanup:

<script setup>
import { onMounted, onBeforeUnmount } from "vue"

const props = defineProps(["authSync"])
let disposed = false
let unsubscribe

onMounted(() => {
  void window.switchfrogConsent.ready.then((client) => {
    if (disposed) return
    unsubscribe = client.onConsentChange((allowed) => {
      props.authSync.invalidate()
      if (allowed) props.authSync.refresh()
    })
  }).catch(props.authSync.reportError)
})

onBeforeUnmount(() => {
  disposed = true
  unsubscribe?.()
  props.authSync.invalidate()
})
</script>

Pass your existing auth synchronizer as authSync. Browser access occurs only after mounting. Keep this component above route content so navigation reuses the subscription. There is no separate Switchfrog framework package to install.

Understand storage and navigation

The CMP owns choices, purpose selection, expiry, and consent evidence. The SDK remembers applied permission in localStorage, scoped to browser origin, storage partition, normalized endpoint, and publishable key. It does not set a consent cookie.

Initial wait writes no preference or probe key. First approval with no saved preference can remain write-free for the preference itself; collection can then write ordinary tracking state. Refusal writes a denied preference when possible. Regrant after denial writes an approval. The preference has no sliding expiry or pageview refresh; it lasts until replaced or site/browser data is cleared. Reset preserves it. Clearing storage is not a regrant API.

A saved SDK denial blocks ordinary installs too. A saved SDK approval cannot bypass a new page's waitForConsent. Apply the CMP's current saved decision on every full navigation, reload, or new tab; a SPA route change does not introduce a new wait for the same client.

With working shared storage, withdrawal coordinates same-scope tabs. A changed preference revision invalidates old work even when a tab missed a rapid denial and regrant. Removing a previously observed record also invalidates it. Peer approval never restarts a blocked tab: that page needs its own fresh CMP grant. Cutoff occurs when the peer observes or checks the change, not atomically across all tabs.

When storage fails

An ordinary fresh client with unreadable preference storage retains its allowed default and runs in page memory. A held client stays held. A denial already observed on the page remains blocked despite later read failures. Readable malformed or unsupported preference data holds collection until an explicit local decision replaces it.

Storage failure alone does not reject optOut() or optIn(). Withdrawal still stops and discards locally; explicit reapproval starts fresh in memory after local cleanup. Once permission storage becomes unavailable, that scope stays in page-memory mode until a full reload, even if storage recovers. It does not restore or persist tracking queues. A later explicit decision may still attempt to save its preference and remove obsolete scoped data.

The SDK reports degraded storage through its existing diagnostics once per affected scope per page. Failure can prevent a choice from surviving navigation or reaching peers. Failed deletion can leave inaccessible old storage behind, and a future page may be unable to recover an unsaved refusal. Do not promise durable refusal or physical erasure when storage fails. Consent-managed pages therefore start held every time and reapply the CMP decision.

Private browsing normally permits temporary localStorage within the private session. The hosted script accesses the customer's page-origin storage; being downloaded from another origin does not make it a third-party iframe. The SDK does not detect private mode or substitute cookies. See MDN Web Storage.

Storage used only to remember consent preferences may qualify for an exception under applicable device-storage rules. That does not automatically exempt tracking identifiers or decide a lawful basis for personal-data processing. Set your policy for your deployment; see the ICO's storage exceptions and storage and access technologies.

Handle completion and errors

onConsentChange(listener) calls the listener synchronously with the current effective boolean and returns an unsubscribe function. It reports permission, not runtime readiness or legal evidence. Ordinary mode can report true before a session exists. Later callbacks occur only when that boolean changes; listener exceptions are isolated and callback return values cannot grant permission.

optOut() closes permission synchronously before returning its promise. It cancels owned requests, stops observation and token insertion, and discards pending telemetry, identity, tokens, and retries instead of flushing them. The promise resolves after local teardown and best-effort scoped storage cleanup. Unexpected required in-memory teardown failures reject while collection stays blocked.

optIn() waits for required local cleanup and resolves after startup. A cleanup failure rejects and leaves collection blocked. A startup network failure rejects after permission was granted; an ordinary start() can retry. A later command that invalidates pending work rejects that work with AbortError. Suppress expected cancellation, and report other failures.

While blocked, valid start() and identify() calls resolve without collecting; identity input is discarded. Token access rejects with NotAllowedError and returns no cached token. Input validation still applies while blocked. reset() remains blocked and does not flush or regrant. See the Browser SDK method reference.

Withdrawal cannot recall transmitted requests, tokens or FormData already returned to your application, accepted server identity associations, historical server data, other origins, or already-running legacy runtimes. Your backend still verifies protected actions and owns its data-retention policy. Avoid reusing application-held tokens after withdrawal.

Use GTM or Cloudflare installations

The current GTM template supports an optional continuing CMP bridge:

  1. Set denied defaults for every required consent type in the CMP's Consent Initialization tag. Google treats unset types as granted.
  2. Enable Let my CMP control collection and choose all required types. Add read permission for each custom type in the template's Permissions tab.
  3. Enable GTM's built-in Event variable. Set Apply current CMP decision (consentDecisionEvent) to a Custom JavaScript variable: function() { return {{Event}} === "switchfrog_consent_decision"; }. It must return an actual boolean, not the string "true".
  4. Fire the same Switchfrog tag once after Consent Initialization, even while denied, so its listener is installed. Do not use Additional Consent Checks that block this bridge or an Always fire override.
  5. After the CMP updates Google's state, emit switchfrog_consent_decision for every saved decision on each page and every update, including denied-to-denied refusal. Refire the tag on that event. Page and ordinary auth events keep the variable false.

All selected types must be granted. The continuing listener can withdraw; only a current CMP saved-decision/update event can grant or regrant. Delayed script completion rereads consent. Supply freshly read identity with an approved event; queued identity is discarded on withdrawal. Without fresh identity, reapproval starts unattributed collection. Additional Consent Checks alone cannot stop an already-running SDK. Use the current template with the current hosted SDK, upgrade all installers, and reload. The template's native callback adapter is internal and is not a public SDK API. See Google's consent APIs.

The managed Cloudflare Zaraz installation is automatic-only. Remove its managed automatic installer before using the direct held SDK and your CMP bridge for the same scope. Managed identity queued before a public hosted client exists cannot observe a hidden ESM client's earlier withdrawal/regrant. That mixed installation is unsupported for consent control. Do not add a direct consent bridge beside the managed installer.

Verify your CMP property

These recipes define callback handling; they do not certify a particular vendor registration, purpose mapping, or customer property. Before using them in production, test a fresh visitor, saved choice, partial approval, refusal, withdrawal, reapproval, repeated callbacks, no CMP, and both CMP/SDK load orders. For Sourcepoint, keep the bridge before its actions in both cases. Delay asynchronous results to check that older approval or identity never wins over a newer withdrawal.

Inspect network traffic and local/session storage, then repeat with reload, SPA navigation, another same-scope tab, and denied storage. A held page may download the SDK but must make no SDK runtime requests or tracking writes. After withdrawal, pending work must be discarded. Reapproval must use current auth without requiring a new login.

Copyright © 2026