Cookiebot captures the user’s category choices, but those categories never reach your custom loader or tag manager — so scripts you inject yourself keep running (or stay blocked) regardless of what the user actually chose in the banner.

Cookiebot’s auto-blocking rewrites <script> tags it recognizes, which lulls teams into assuming everything is gated. But any script you inject at runtime, hand off to Google Tag Manager, or load through your own consent gate is invisible to auto-blocking. Those consumers need the consent state pushed to them explicitly, and Cookiebot exposes exactly the events and object to do that.

Triage: Confirm Categories Never Reach Your Loader

If the consent booleans flip correctly but your custom-loaded tags ignore them, the root cause below applies.

Cookiebot’s auto-blocking rewrites <script> elements it recognises, which is why it appears to work until it meets markup it cannot recognise. The rewrite happens in the DOM before execution; anything that never appears as a matching element in the DOM passes straight through.

Auto-blocking sees elements, not intentions Three ways a vendor is loaded, passing through the Cookiebot auto-blocking layer. A recognised script element in the initial HTML is rewritten to type text plain and held until consent — intercepted. A script element created at runtime by your own code is appended after the rewrite pass and is not intercepted. A direct fetch or beacon call is not a script element at all and is never seen. Only the first path is protected, so the other two must be gated by your own code. auto-blocking rewrite pass matches known vendor URLs in the DOM <script src="known"> held until consent ✓ createElement('script') appended after the pass ✗ fetch() / sendBeacon() not a script element ✗ Auto-blocking is a safety net for markup you did not write. It is not a gate for code you did.

Treat auto-blocking as defence in depth rather than as the mechanism. Your own injection paths — anything created in JavaScript, and every direct network call to a vendor endpoint — must consult the consent object explicitly, because nothing in the rewrite pass will ever see them.

Cookiebot ships two independent mechanisms, and teams conflate them. Auto-blocking works by rewriting known third-party <script> tags — Cookiebot changes their type to text/plain and adds a data-cookieconsent category, then re-enables them once the matching category is granted. This only governs static tags present in the HTML that Cookiebot recognizes. The consent object plus its events is the programmatic interface: window.Cookiebot.consent holds the current booleans, and Cookiebot dispatches DOM events when that object changes.

Any script your application injects at runtime — a loader gated behind a feature flag, a tag pushed through Google Tag Manager, an SDK you import() dynamically — never passes through auto-blocking’s HTML rewrite. Auto-blocking simply cannot see it. If your only integration is auto-blocking, those runtime consumers run ungoverned, which is both a compliance gap and the reason your loader ignores the user’s decision.

The correct model is the one described in the CMP selection and integration guide: Cookiebot is the single source of truth, and every consumer — including your custom loader — subscribes to its change events and reads its consent object rather than assuming auto-blocking covers them. When several vendors depend on that state, route it through a broker as covered in syncing consent states across multiple vendors; the subscription primitive is identical either way.

Cookiebot exposes three DOM events on window, plus the Cookiebot.consent object:

  • CookiebotOnConsentReady — fires once the consent state is known, on every page load. This is where load-time replay of a returning visitor’s saved decision belongs. Fires whether the user accepts, declines, or a prior decision is restored from the cookie.
  • CookiebotOnAccept — fires when the user accepts one or more categories. Read Cookiebot.consent inside it to see exactly which.
  • CookiebotOnDecline — fires when categories are declined. Use it to skip or tear down anything gated on a now-denied category.

The consent object has one boolean per category: Cookiebot.consent.necessary, .preferences, .statistics, and .marketing.

Resolution: Subscribe to Cookiebot Events, Gate on the Booleans

Define one apply function that reads Cookiebot.consent and injects only what is granted, then wire it to CookiebotOnConsentReady (load-time replay) and CookiebotOnAccept/CookiebotOnDecline (live changes).

// consent-sync-cookiebot.js — load AFTER the Cookiebot loader script tag.
// The Cookiebot script tag itself carries data-cbid="YOUR-DOMAIN-GROUP-ID".

// 1. Map Cookiebot's category booleans to your injection gates.
//    Cookiebot's categories are fixed: necessary, preferences,
//    statistics, marketing.
function currentConsent() {
  // Cookiebot.consent may not exist until the SDK is ready; default to
  // all-denied so nothing leaks before the state is known.
  const c = (window.Cookiebot && window.Cookiebot.consent) || {};
  return {
    statistics: c.statistics === true,
    marketing:  c.marketing === true,
    preferences: c.preferences === true,
  };
}

// 2. Idempotent apply: safe to call from every event.
const injected = new Set();
function applyConsent() {
  const consent = currentConsent();

  if (consent.statistics && !injected.has('statistics')) {
    injected.add('statistics');
    injectScript('https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX'); // your GA4 id
  }
  if (consent.marketing && !injected.has('marketing')) {
    injected.add('marketing');
    injectScript('https://connect.facebook.net/en_US/fbevents.js');
  }
  // Add further categories as needed. A false boolean must never inject.
}

function injectScript(src) {
  const s = document.createElement('script');
  s.src = src;
  s.async = true;
  document.head.appendChild(s);
}

// 3. Subscribe. CookiebotOnConsentReady runs on every load once state is
//    known — this replays a returning visitor's saved decision.
window.addEventListener('CookiebotOnConsentReady', applyConsent);

// CookiebotOnAccept / CookiebotOnDecline fire on live user changes.
window.addEventListener('CookiebotOnAccept', applyConsent);
window.addEventListener('CookiebotOnDecline', function () {
  // Re-evaluate: a declined category should not inject. Teardown of an
  // already-loaded SDK is handled separately (see revocation guide).
  applyConsent();
});

For static tags you do want Cookiebot to auto-block, mark them declaratively instead of injecting them — set type="text/plain" and the category:

<!-- Cookiebot auto-blocks this until the marketing category is granted -->
<script
  type="text/plain"
  data-cookieconsent="marketing"
  src="https://connect.facebook.net/en_US/fbevents.js">
</script>

Use the declarative data-cookieconsent form for tags that live in your HTML, and the event-driven applyConsent() for anything you inject at runtime. The two approaches coexist; the mistake is assuming auto-blocking covers the runtime case.

Live revocation of a category whose SDK already loaded needs an explicit teardown rather than just skipping injection — that path is covered in handling consent revocation without a page reload.

The four booleans, and when they are trustworthy

Cookiebot exposes its decision as four booleans on a global object, and every one of them reads false before the platform has resolved. That is indistinguishable, to a naive if, from a visitor who refused — which is why the guard has to test resolution before it tests the category.

False can mean "no" or "not yet" Two snapshots of the Cookiebot consent object. Before resolution, the necessary flag is true and the preferences, statistics and marketing flags are all false, and the ready flag is false. After resolution with the visitor accepting statistics only, necessary and statistics are true while preferences and marketing are false, and the ready flag is true. The two snapshots are identical for the statistics flag alone, so a guard that reads a category without first checking the ready flag cannot tell refusal from an unresolved state. before resolution necessary: true preferences: false statistics: false marketing: false ready: false after resolution necessary: true preferences: false statistics: true marketing: false ready: true Look at marketing alone: false in both. Only the ready flag distinguishes "refused" from "not asked yet". Gate on the event, not on a poll, and the ambiguous window never reaches your code.

This is the same ambiguity that makes a boolean a poor consent primitive generally: a two-valued flag cannot represent a three-valued state. Normalise to an explicit granted | denied | pending value at the adapter boundary and the rest of your code stops needing to remember the distinction.

Verification

A marketing request that appears only after accept, combined with Cookiebot.consent.marketing flipping from false to true, confirms the sync works end to end.

Categories are fixed, your vendors are not

Cookiebot’s four categories are fixed by the product, so the mapping work is on your side: every vendor you load has to be assigned to one of them, and the assignment is a judgement about what the vendor does rather than about what it is called. A misfiled vendor is a compliance defect that no amount of correct wiring detects.

Filing vendors into four fixed categories Four fixed categories with their correct occupants and the vendor most often misfiled into each. Necessary holds session and security vendors, and is commonly abused by filing analytics there to avoid the gate. Preferences holds language and layout persistence, and is commonly given personalisation tools that also profile. Statistics holds measurement and real-user monitoring, and is commonly given session replay, which records content. Marketing holds advertising and remarketing, and is commonly missed for social embeds that set advertising identifiers. necessary session, CSRF, load balancing often abused to smuggle analytics past the gate preferences language, theme, layout memory often given personalisation tools that also profile statistics measurement, RUM, error tracking often given session replay, which records page content marketing ads, remarketing, pixels often misses social embeds that set ad identifiers File by what the vendor does with the data, not by which team asked for it. Record the reasoning next to the mapping: the justification is what an audit asks for, not the code.

Keep the mapping and its justification in the same file. When a regulator or an internal review asks why a vendor sits in a given category, the answer needs to be a sentence someone wrote deliberately, not an inference from a config value that nobody remembers setting.

Common Pitfalls

  • Assuming auto-blocking covers runtime-injected scripts. Auto-blocking only rewrites tags present in the initial HTML that Cookiebot recognizes. Anything you createElement('script') or load through GTM at runtime bypasses it entirely and must be gated on Cookiebot.consent.
  • Reading Cookiebot.consent before CookiebotOnConsentReady. The object may be absent or stale until the ready event fires. Reading it during your loader’s synchronous init returns undefined booleans, so gates evaluate to denied and nothing ever loads. Read it inside the event handlers.
  • Registering listeners after Cookiebot has already dispatched. If your subscription code runs after CookiebotOnConsentReady has fired, you miss the load-time replay. Register the listeners as early as possible, or call applyConsent() once immediately after registering as a catch-up.

Related

Up: Selecting and Integrating a Consent Management Platform