Feature · consent capture
The banner is the part everybody looks at. The part that decides whether you are actually compliant is the twelve lines above it in the <head>, and what those lines do to every other tag on the page. This is that half of the job: placement, load order, the three blocking mechanisms, and the one of them that is genuinely airtight.
Part of
The widget is the capture surface. The module is the record behind it — purposes, artefacts, withdrawal and the audit trail a regulator would ask for.
A script that has already executed cannot be un-executed. If Google Analytics loads in line 4 of your head and the consent widget loads in line 40, the pageview has been sent, the cookie has been written, and no amount of banner UI undoes it. This is the single most common reason a site with a perfectly good-looking banner fails a scan.
So the embed instruction is positional, not decorative: the tag goes first in the head, above every analytics, advertising and marketing tag on the page. The widget finds its own script element through document.currentScript, and falls back to scanning for a script whose source ends in widget.js and which carries the publishable key — which means async and defer both work, and which means a caching plugin that rewrites src to data-src does not break the embed either.
There are two, and only two. A publishable key, which identifies the tenant and the configuration, and an optional mode that picks the surface — banner, popup, mini, floater or QR. Mode is read at runtime, so the same key can render a full banner on the marketing site and a discreet floater inside the logged-in app.
Everything else that a competitor would make you set as an attribute — position, offsets, pill label, modal title and body, which cookie categories are visible, which purposes are in scope, which languages are offered, whether an email is required — is configuration held against the key and fetched at load. That is a deliberate trade. Attributes mean a deploy on every property to change a word of banner copy; server-held configuration means one change in the dashboard and forty sites pick it up on their next page view.
The key is a publishable one. It identifies a tenant, it is meant to sit in the page source, and it grants nothing beyond rendering the banner and posting a consent decision. One caveat worth knowing rather than discovering: origin allow-listing on a key defaults open — an empty allowed-origins list accepts any origin, which matches how the large CMPs behave but is not what most people assume. Set the list.
Blocking is not one mechanism. It is three, layered, and they have genuinely different reliability. Anyone who tells you their banner "blocks everything" is describing the first layer and hoping you do not ask about the other two.
The registry ships seeded with the vendors that actually appear on Indian sites, classified rather than merely listed: the major analytics suites and product-analytics tools under analytics; the advertising pixels — Meta, LinkedIn, Google Ads, TikTok, X — under marketing; live chat and marketing automation under functional. Four categories exist in the registry and a fifth, uncategorised, exists in the banner so that a cookie nobody has classified yet is visible rather than silently treated as necessary.
You can extend it. The point of it being tenant data rather than a hard-coded list is that the vendor your growth team added last week can be classified without waiting for a platform release.
For anyone running Google tooling, the widget writes the Consent Mode v2 default state before any measurement tag has a chance to read it: ad storage, analytics storage, ad user data, ad personalisation and functionality storage all denied, security storage granted, with a short wait-for-update window so the tags pause rather than assume. Every subsequent decision — accept, reject, or a per-category change in the preference centre — pushes a consent update.
This matters on the Indian obligation as much as the Google one. Consent under §6 has to be a clear affirmative action for a specified purpose. A default state of "granted until told otherwise" is not that, regardless of what the banner says.
The widget exposes a small global and one event. The global opens the preference centre, reads the current decision, and clears it locally. The event fires on every persisted decision and carries the state, which is how you initialise a tag that has to be created in JavaScript rather than declared in HTML — subscribe to the event, initialise there, never on DOM ready.
Global Privacy Control and Do Not Track are read on load. Where the browser signals either, the widget treats it as a rejection rather than showing a banner that asks a question the visitor has already answered at the browser level.
The decision itself lives in both a first-party cookie and local storage, carrying the choice, the per-category state, the timestamp, a short human-readable receipt key and an expiry. The expiry is a year from the decision, and an expired decision counts as no decision — the banner re-prompts rather than coasting on a consent given fourteen months ago, which is the behaviour §6 implies and most banners do not implement.
Two identifiers sit alongside it, both first-party, both six months: one that ties repeat visits from the same browser to the same consent record so a returning visitor is not asked again, and one anonymous handle used when a visitor interacts with the preference centre without identifying themselves. A session flag holds the age-gate answer for the session only.
There is no route-change hook. No history listener, no hash listener. That is worth saying plainly because plenty of CMP documentation implies otherwise and then quietly relies on the same thing this one does: the banner is rendered once into the document rather than into a route, the mutation observer keeps working across client-side navigations because it is watching the document element, and the persistent pill stays mounted after a decision so the preference centre is always one click away — which is the §6(4) requirement anyway.
For a React or Vue application the practical rule is the one above: initialise measurement inside the consent event listener, not in a component lifecycle method. A component that mounts on route change and initialises analytics will do it again on every route change, consent or not.
It cannot un-execute a tag that loaded before it. It cannot see server-side tagging or a conversions API call made from your backend — those are outside the browser entirely and need a consent check in your own code, which is what the consent API is for. It cannot classify a vendor nobody has classified.
And it does not ship content-security-policy guidance today. If you run a strict script-src, you will need to allow the API host yourself; there is no nonce integration and no subresource-integrity hash published for the bundle. That is a real gap rather than a feature, and it is better to read it here than to find it in a CSP report.
It ran, and nothing can undo that. The cookie purge will delete cookies it wrote for categories the visitor has not consented to, on that load and after every decision, which limits the damage — but the network request was made. This is why the embed instruction is positional rather than a suggestion, and why a scan reports pre-consent network hosts separately from pre-consent cookies.
The widget must load in the page head above the GTM container snippet, not as a tag inside GTM. A consent layer deployed through the tag manager it is meant to gate has already lost the race. Once it is placed correctly it writes Consent Mode v2 defaults before the container initialises and pushes an update on every decision, so GTM-managed tags respect the choice without individual blocking rules.
The surface can vary per page, because the mode is a tag attribute read at runtime — a full banner on the marketing site and a floater inside the product using the same key. Copy, position, purposes, languages and visible cookie categories are held against the key, so genuinely different banners want separate keys, each with its own origin allow-list.
A year from the decision. The expiry is stored with the decision, and an expired record is treated as no record at all, so the banner re-prompts rather than continuing to rely on it. Withdrawal, by contrast, is immediate and is not tied to that clock.
No. The tag carries a key and an optional mode; wording, position, offsets, visible categories, purposes and languages are configuration fetched at load. Changing them is a dashboard change that every embedded property picks up on its next page view, which is the main reason they are not attributes.
Describes how ProtectComply implements one obligation under the DPDP Act, 2023 and the DPDP Rules. General information, not legal advice.
Dinkar Singh — Dinkar covers privacy engineering at ProtectComply — discovery, consent propagation and the evidence trail behind them.
Scan a live URL and get back every script that fires before a visitor has chosen anything.
Scan a site →