OneTrust consent recipe

View as Markdown

Start the SDK paused until consent, then grant or withdraw from the OneTrust group you assign to Telltale. This recipe uses OneTrust's public browser API: the OneTrustGroupsUpdated event, the OnetrustActiveGroups variable and OneTrust.IsAlertBoxClosed().

Choose the group

Group IDs come from the cookie categories configured for your OneTrust domain. There is no universal ID: C0002 is often Performance, but confirm the ID in your own configuration and put the SDK in a group that requires consent (not an Always Active group). The recipe reads the group from the script tag, so the file stays the same across properties:

HTML
<script src="/telltale-onetrust.js" data-group="C0002"></script>

Recipe

Save this code as /telltale-onetrust.js on your own origin (the Telltale server does not serve recipe files) and load it after the SDK tag, or after the page-owned bridge from the Consent guide (nonce it under a nonce policy; see the CSP guide). When the bridge is present the file sends decisions through it and creates no client of its own.

JavaScript
// /telltale-onetrust.js
(() => {
  const GROUP = document.currentScript?.dataset.group;
  // With the page-owned bridge (Consent guide) loaded first, decisions go through it; otherwise this file owns the client.
  const bridge = window.telltaleConsent;
  const client = bridge ? null : window.Telltale.init({
    endpoint: 'https://app.trytelltale.dev',
    key: 'YOUR_SITE_PUBLISHABLE_KEY',
    waitForConsent: true,
    discover: true
  });
  const decide = bridge
    ? allowed => bridge.applyConsent(allowed)
    : allowed => (allowed ? client.optIn() : client.optOut()).catch(console.error);
  if (!/^[A-Za-z0-9_-]{1,64}$/.test(GROUP || '')) {
    console.error('Telltale: set data-group to your OneTrust group ID; collection stays held');
    return;
  }

  let decided = false; // a valid decision has been applied on this page
  const activeGroups = value => (Array.isArray(value) ? value : String(value ?? '').split(',')).map(s => String(s).trim()).filter(Boolean);
  function sync(groups) {
    const oneTrust = window.OneTrust;
    if (typeof oneTrust?.IsAlertBoxClosed !== 'function') return;
    const valid = oneTrust.IsAlertBoxClosed() === true;
    // Before any decision: stay held. Unknown is never treated as consent.
    if (!valid && !decided) return;
    decided = true;
    // Losing a valid decision after one was applied is a withdrawal.
    decide(valid && activeGroups(groups ?? window.OnetrustActiveGroups).includes(GROUP));
  }

  // Fires when OneTrust loads (in current versions) and whenever preferences are saved.
  let eventSeen = false;
  window.addEventListener('OneTrustGroupsUpdated', event => {
    eventSeen = true;
    sync(event.detail ?? event.details);
  });

  // OneTrust may have loaded before this file ran, and older versions only call OptanonWrapper on load. Wait, by reading
  // only, until its API is ready (the readiness rule of /adapters/v1/cmp.js), then apply the saved decision once.
  const sdkExpected = () => Array.from(document.scripts || []).some(s => /cookielaw\.org|otSDKStub|otBannerSdk/.test(s.src || s.textContent || ''));
  const apiReady = () => typeof window.OneTrust?.IsAlertBoxClosed === 'function' && typeof window.OnetrustActiveGroups === 'string' && (!!document.querySelector?.('#onetrust-style') || !sdkExpected());
  const started = Date.now();
  (function waitForOneTrust() {
    if (eventSeen) return; // OneTrust already reported; the event path owns decisions from here
    if (apiReady()) return sync();
    if (Date.now() - started > 10000) return console.warn('Telltale: OneTrust is not ready; collection stays held until it reports a decision');
    setTimeout(waitForOneTrust, 100);
  })();
})();

The readiness wait only reads OneTrust's state: it never clicks the banner and never calls a OneTrust method other than IsAlertBoxClosed(). When the page loads the OneTrust SDK, the file waits until the SDK has injected its #onetrust-style element, because the stub can define OnetrustActiveGroups before the real decision is known. If OneTrust has not loaded after 10 seconds the file logs a warning and stays held; a later OneTrustGroupsUpdated event still applies the decision.

OneTrust documents the event payload as both detail and details, so the recipe accepts either and falls back to OnetrustActiveGroups (a comma-separated list). Exact membership is checked after splitting, so C0002 never matches C00021.

Notes

IsAlertBoxClosed() reports whether a valid decision exists, not whether the group is approved. Before the first valid decision the recipe stays held. Once a decision has been applied on the page, a later state without a valid decision (for example OneTrust reconsent or an expired choice) withdraws, because the previous approval no longer stands. Use denied defaults for explicit-consent regions.

If your site already defines OptanonWrapper, keep it; OneTrust calls it on load and when consent changes. You can call sync() from inside it instead of adding the event listener, but use one path, not both.

Withdrawal calls optOut(), which stops owned collection and tools and discards pending evidence. It cannot recall data already sent. The recipe reapplies the saved decision on every full page load. If your legal basis allows collection before a choice in some regions, make that decision explicitly in sync(); do not remove the held start to achieve it.

Logging in never grants consent. Keep identity (identify/reset, or connectIdentity from the Consent guide) separate from this consent bridge.