Read the consent state from cookieconsent.cookieConsentObject in the browser, or from the cookie_consent_level cookie on your server. The object and the cookie both hold the 4 categories as true or false. Use the state to load a tool at once for visitors who accepted its category, and print the tool as a tagged script for everyone else.

Reading the state takes 3 steps and about 15 minutes per server language: learn the cookie format, read the state where your code runs, then verify it in Chrome DevTools. You need Cookie Consent installed and a list of the categories your site uses.

The cookie_consent_level cookie holds a URL-encoded JSON object with the 4 categories, and cookie_consent_user_accepted holds true once the visitor answered. The object is never an array, and it always lists the categories in the order strictly-necessary, functionality, tracking, targeting.

Compare a raw value with the same value decoded, because some server languages hand your code the raw form.

cookie_consent_level=%7B%22strictly-necessary%22%3Atrue%2C%22functionality%22%3Afalse%2C%22tracking%22%3Atrue%2C%22targeting%22%3Afalse%7D

{"strictly-necessary":true,"functionality":false,"tracking":true,"targeting":false}

The cookie_consent_level cookie exists before the visitor answers. Cookie Consent writes it on the first page load with the categories released on page load, which is strictly-necessary alone in express mode without page_load_consent_levels. Treat a category as accepted only when the visitor answered and the category is true and in use. A category your site left out of notice_banner_purposes_levels counts as refused, even where an older cookie holds true for it.

Several frameworks decode cookie values before your code sees them. Decoding the value once more does no harm, because the decoded JSON does not hold a % character, so the helpers always decode. Check the attributes and lifetime of each cookie in the 3 cookies Cookie Consent writes.

In the browser, read the running instance. On the server, parse the cookie the browser sends with each request.

cookieconsent.cookieConsentObject.userConsent.acceptedLevels holds the current choice, and userConsent.userAccepted tells whether the visitor answered. The object is undefined until Cookie Consent starts, and under the TermsFeed loader that happens after the script file arrives.

The cookieConsentAccepted() function reads the instance when it exists and falls back to the cookie before that.

function cookieConsentAccepted(category) {
  var cc = window.cookieconsent && window.cookieconsent.cookieConsentObject;
  if (cc) {
    return cc.userConsent.userAccepted === true && cc.userConsent.acceptedLevels[category] === true;
  }
  var cookies = {};
  document.cookie.split(';').forEach(function (pair) {
    var index = pair.indexOf('=');
    if (index > -1) {
      cookies[pair.slice(0, index).trim()] = pair.slice(index + 1).trim();
    }
  });
  if (cookies['cookie_consent_user_accepted'] !== 'true' || !cookies['cookie_consent_level']) {
    return false;
  }
  try {
    var levels = JSON.parse(decodeURIComponent(cookies['cookie_consent_level']));
    return levels[category] === true;
  } catch (error) {
    return false;
  }
}

// Example: send an event only for visitors who accepted tracking
if (cookieConsentAccepted('tracking') && typeof gtag === 'function') {
  gtag('event', 'newsletter_view');
}

acceptedLevels includes switch changes the visitor has not saved while the Preferences Center is open. Code that must follow the saved answer alone reads the state once when it starts, before any switch changes. It then listens for the cc_userConsentSaved event, whose details property holds each answer saved later. On later pages of a visitor who answered, the event fires on page load only with user_consent_saved_on_page_load on, so the first read carries the earlier answer. Find the event and its properties in Free Cookie Consent Callbacks and DOM Events.

Neither the cookie fallback in cookieConsentAccepted() nor the server helpers in PHP, Python, Node.js, Ruby, and C# know which categories your configuration uses. Pass each of these functions only categories listed in your notice_banner_purposes_levels.

PHP decodes cookie values in $_COOKIE, and rawurldecode in the helper leaves an already decoded value unchanged. With the cookie_consent_accepted() helper, the page prints the Google tag as a running script for visitors who accepted tracking. Everyone else gets it as a tagged script, which Cookie Consent releases once the visitor accepts tracking.

<?php
function cookie_consent_accepted(string $category): bool
{
    if (($_COOKIE['cookie_consent_user_accepted'] ?? '') !== 'true') {
        return false;
    }
    $levels = json_decode(rawurldecode($_COOKIE['cookie_consent_level'] ?? ''), true);
    return is_array($levels) && ($levels[$category] ?? false) === true;
}

$ga_config = "window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', 'G-XXXXXXX');";

if (cookie_consent_accepted('tracking')) {
    echo '<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX"></script>';
    echo '<script>' . $ga_config . '</script>';
} else {
    echo '<script type="text/plain" data-cookie-consent="tracking" async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX"></script>';
    echo '<script type="text/plain" data-cookie-consent="tracking">' . $ga_config . '</script>';
}

Python frameworks hand over the raw cookie string, so decode it with urllib.parse.unquote and parse it with json.loads. One helper takes the request cookies in either framework.

import json
from urllib.parse import unquote

def cookie_consent_accepted(cookies, category):
    if cookies.get("cookie_consent_user_accepted") != "true":
        return False
    try:
        levels = json.loads(unquote(cookies.get("cookie_consent_level", "")))
    except ValueError:
        return False
    return isinstance(levels, dict) and levels.get(category) is True

# Django view:  cookie_consent_accepted(request.COOKIES, "tracking")
# Flask view:   cookie_consent_accepted(request.cookies, "tracking")

The cookie-parser middleware decodes cookie values into req.cookies, and a small helper parses the JSON. This minimal Express app reads the tracking category.

const express = require('express');
const cookieParser = require('cookie-parser');

const app = express();
app.use(cookieParser());

function cookieConsentAccepted(cookies, category) {
  if (cookies.cookie_consent_user_accepted !== 'true') {
    return false;
  }
  try {
    const levels = JSON.parse(decodeURIComponent(cookies.cookie_consent_level || ''));
    return levels !== null && levels[category] === true;
  } catch (error) {
    return false;
  }
}

app.get('/', (req, res) => {
  const tracking = cookieConsentAccepted(req.cookies, 'tracking');
  res.send(`Tracking accepted: ${tracking}`);
});

app.listen(3000);

In Ruby on Rails, the cookies object holds decoded cookie values, and a helper in ApplicationHelper parses the JSON for every view. Call the helper from any view with cookie_consent_accepted?(:tracking).

# app/helpers/application_helper.rb
require "cgi"
require "json"

module ApplicationHelper
  def cookie_consent_accepted?(category)
    return false unless cookies[:cookie_consent_user_accepted] == "true"

    levels = JSON.parse(CGI.unescape(cookies[:cookie_consent_level].to_s))
    levels.is_a?(Hash) && levels[category.to_s] == true
  rescue JSON::ParserError
    false
  end
end

# In a view: <% if cookie_consent_accepted?(:tracking) %> ... <% end %>

ASP.NET Core decodes cookie values in Request.Cookies, and JsonDocument reads the property names that hold a hyphen. The static CookieConsentState.Accepted() helper serves Razor pages and controllers alike.

using System;
using System.Text.Json;
using Microsoft.AspNetCore.Http;

public static class CookieConsentState
{
    public static bool Accepted(HttpRequest request, string category)
    {
        if (request.Cookies["cookie_consent_user_accepted"] != "true")
        {
            return false;
        }
        var raw = request.Cookies["cookie_consent_level"];
        if (string.IsNullOrEmpty(raw))
        {
            return false;
        }
        try
        {
            using var document = JsonDocument.Parse(Uri.UnescapeDataString(raw));
            return document.RootElement.ValueKind == JsonValueKind.Object
                && document.RootElement.TryGetProperty(category, out var value)
                && value.ValueKind == JsonValueKind.True;
        }
        catch (JsonException)
        {
            return false;
        }
    }
}

// In a Razor page or controller: CookieConsentState.Accepted(Request, "tracking")

Server-side checks see the cookie only from the request after Cookie Consent wrote it, so the page where the visitor answers still shows the state from before. The cookies carry SameSite=Strict, so the browser leaves them off the first request of a visitor who arrives from another site, such as a search result. Your server reads that first page as not answered. For visitors who have not accepted, print the tagged script instead of nothing, so Cookie Consent releases the tool on the page where they answer. A full-page cache that serves one copy to every visitor ignores the cookie, so exclude those pages from the cache or keep the decision in the browser.

Compare what your code returns with the cookies the browser holds. Publish your code and clear any page cache, then close every private window, open a new one, open Chrome DevTools, and load your site.

  1. Select the Application tab, then Storage, Cookies, and your site. The list does not hold a cookie_consent_user_accepted cookie before you answer.
  2. Click the "Change my preferences" button and select the tab of a category your site uses, such as "Tracking cookies". Set its switch to Active, and click the "Save my preferences" button. cookie_consent_user_accepted now reads true.
  3. Select the Network tab, type your page path (such as /pricing) in the filter box, or select the Doc filter, and reload. Click the document request and read the Cookie request header, which carries the 3 Cookie Consent cookies to your server.
  4. Check that your server code or the browser function returns true for the category you turned on and false for the other optional categories. A switch that was already Active when you opened the Preferences Center saves true, as for a category in page_load_consent_levels or, by default, every category in implied mode.
  5. In the same window, open a page of another site, select the Console tab, and type location.href = 'https://www.example.com/pricing' with the address of your page. On the Network tab, the Cookie header of the document request does not carry a Cookie Consent cookie, so your server code returns false for every optional category there. The browser function still returns true for the category you turned on.

Each of these mistakes lets your code act on a state the visitor did not choose, so search your code for them.

  • The code trusts cookie_consent_level alone. Check cookie_consent_user_accepted first, because only that cookie shows that the visitor answered.
  • The code parses the value as an array. Parse the value as a JSON object keyed by category ID.
  • The code skips decoding in a language that keeps the raw value. Decode with decodeURIComponent, unquote, or the equivalent before parsing.
  • The code expects scripts to stop on the same page after a refusal. The cookie changes at once, but scripts that already ran keep running until the next page load. Turn on page_refresh_confirmation_buttons to reload the page after each answer, so a withdrawn category stops at once.