Cookie Consent Modal
Embed the consent banner that a Neostra collection point publishes, wire up its signals, block your own tags, and react to consent in JavaScript.
The consent modal is not a standalone product. It is the browser side of a Collection Point in Consent Management. Everything the modal does on your site is decided by what the collection point owner configured and published: which banner appears for which regulation, which categories and tracking services exist, which languages are offered, which signals fire, and which attributes are captured. The script itself carries no settings of its own beyond the collection point ID.
This page is for the developer who embeds the script. It explains what you receive, what must already be in place on the collection point, and what you can observe and hook into in the browser.
How the modal fits in Consent Management
A collection point represents one place where consent is collected, usually one website domain. It is created and edited in the Neostra console under Consent Management > Collection Points. The collection point, not the modal, is the unit of configuration.
Changes are made to a draft. Nothing reaches visitors until the owner clicks Publish at the top of the collection point. Each publish creates a new entry under Published History, with the version name, who published it, and when. Discard Draft throws away unpublished edits. The Scripts page (menu section INTEGRATION) shows the embed snippet only once the collection point is published and a banner is attached.
When the script runs on a visitor's browser, it asks Neostra for the published configuration of the collection point named in the snippet. Neostra looks up the visitor's country and state from their IP address, matches that location against the jurisdiction rules on the collection point, and returns the banner that applies, together with the categories, tracking services, translations, signals, and attributes that belong to that collection point.
As the developer, you receive from the collection point owner:
- The embed snippet copied from Scripts. It contains the collection point ID and, when available, a Subresource Integrity hash.
- The public Preference Center URL, if a preference center is attached, also shown on Scripts.
- The category keys the owner defined (for example
analytics,marketing). You need these to react to consent in your own code or in your tag manager. - Which signals are enabled under Signals, because some of them require your
gtag.jsor Google Tag Manager container to load before the modal.
Before you embed
The collection point owner configures everything the modal does in Neostra. Before you add the script, confirm with them that the following are in place. Each item links to the administrator guide.
- The collection point's Domain is the site you are embedding on, and a Default Banner is selected. See Collection Points.
- Tracking services are registered and confirmed under Banner > Tracking Services, with categories and their keys defined. The keys are what you see in consent signals, and only confirmed services are used by the auto-blocker.
- Translations are complete for every language the banner offers. The modal refuses a configuration with missing translations.
- The signals your site needs are turned on under Signals. Each has prerequisites on your page; see Signals for developers.
- If you rely on a User Identifier attribute, the browser cookie it reads exists on your page. See Attributes.
- The collection point is published. The snippet is only available for a published collection point.
Category keys, the consent model, and the set of tracking services are part of the published configuration. If the owner changes them and publishes again, returning visitors are asked to consent again. See Re-solicitation.
Embedding the snippet
Get the snippet from the collection point
Ask the owner to open the collection point, go to Scripts, and copy the Banner snippet. A green Integrity protected chip means the snippet includes a Subresource Integrity hash. The copy button is disabled for a moment while the hash is being fetched; wait for it.
Add it to every page
Paste the snippet into the <head> of every page, before any script you want the banner to control and after gtag.js or your Google Tag Manager container if Google Consent Mode or Tag Manager Integration is enabled. The script initializes itself as soon as it loads.
Test on HTTPS
The consent cookie is written with the Secure flag, so decisions only persist on HTTPS pages. Test on an HTTPS staging site, not on plain HTTP.
The snippet has this structure. The real src, integrity, and collection point ID come from the Scripts page. Do not copy the placeholder below.
<script
data-cp-id="<your-collection-point-id>"
data-neo-ignore
src="https://<modal-host>/<modal-bundle>.<hash>.js"
integrity="sha384-<hash>"
crossorigin="anonymous"
></script>
| Attribute | Meaning |
|---|---|
data-cp-id | Required. Identifies the collection point whose published configuration to load. Without it the script logs data-cp-id attribute is required and does nothing. |
src | The script file. When the Integrity protected chip is shown, the filename contains a content hash and never changes once published. |
integrity, crossorigin | Subresource Integrity. The browser refuses to run the file if its contents do not match the hash. Because the hashed file is immutable, the hash stays valid until you copy a new snippet. |
data-neo-ignore | Present only when Auto-Blocker is enabled on Signals. It tells the auto-blocker to leave the modal script itself alone. Keep it. |
data-mode="preview" | Optional, added by you. Loads the configuration in preview mode. See Preview mode. Remove it in production. |
config-url | Optional, added by you. Load the configuration from a URL you specify instead of from Neostra. For local testing only. |
data-config-json | Optional, added by you. Pass the configuration inline as a JSON string. Takes precedence over config-url. For local testing only. |
Immutable hashed file or latest build
The Scripts page prefers a content-hashed, immutable copy of the script and attaches an integrity hash to it. Your page then runs exactly the build that was current when the snippet was copied, and the browser verifies it on every load. To move to a newer build, copy the snippet again.
If the hash information is not available at the moment the page is opened, the Integrity protected chip is missing and the snippet falls back to the plain script URL with no integrity attribute. That URL always serves the latest build, so it updates automatically but without integrity verification. If you want the protected form, reload the Scripts page and copy again.
Never edit src or integrity by hand or combine them from two different copies of the snippet. A mismatch makes the browser block the script and the banner never appears.
Preview mode
Add data-mode="preview" to the snippet on a staging page to check text, layout, languages, and category lists against an unpublished or partially configured collection point.
In preview mode:
- Configuration validation problems are logged as console warnings instead of stopping the modal, and a summary line reports how many issues would fail in production.
- Accept, Reject, Customize, Save, Cancel, close, and backdrop clicks take no action. No consent is stored and no signals fire.
The Preview dialog in the console uses this same mode. Remove the attribute before going live.
Consent cookie
The decision is stored in a first-party cookie on your domain named neo-cmp, valid for 365 days, with SameSite=Lax and Secure. The value is the JSON consent state described under Consent state shape. If the visitor's browser already holds a valid decision, the banner is not shown and the stored state is applied on load.
When your subscription includes consent record storage, the same consent state is also sent to Neostra as a consent receipt after each decision.
Signals for developers
Signals are switched on per collection point under Signals. Each one has a toggle, and changes take effect only after the collection point is published. The cards below use the titles and guidance shown in the console, followed by what you can observe in the browser.
Auto-Blocker
"Blocks third-party scripts until the user gives consent."
What it is: intercepts all external <script>, <iframe>, and <img> tags before the user has consented. Resources are released automatically when consent is given for their category. Essential category scripts are always unblocked, even when the user declines all.
Enable if your site includes third-party scripts directly in HTML, you want automatic blocking without relying on a tag manager, and services are configured and categorised under Tracking Services. Precise per-category unblocking requires each service's host to be registered; otherwise all resources are released on consent as a safe fallback.
Skip if all tags are already controlled via Google Tag Manager or similar tools, or your site uses an opt-out consent model (blocking is disabled in opt-out mode).
Setup steps shown in the console:
- Enable this toggle and publish.
- Configure services under Banner > Tracking Services and assign each to the correct category. This determines which scripts are released per consent choice.
- Add
data-neo-ignoreto any element that must never be blocked (for example a core site script that does not require consent). - Verify in DevTools: blocked elements show
data-neo-block-srcinstead ofsrcuntil consent is given.
What you observe: the modal script carries data-neo-ignore in the snippet. Held-back elements have their src moved to data-neo-block-src. The behaviour is described in full under Blocking your own tags.
Google Consent Mode v2
"Adjusts Google tag behaviour in real-time based on the user's consent choices."
What it is: sends the user's consent choices to Google tags, and lets Google Analytics and Google Ads adjust tracking and ads based on those choices.
Enable if you use Google Analytics or Google Ads, or you need consent-aware tracking for EU/UK users.
Prerequisites: Google tags must be set up and load before the consent banner (either using gtag.js or Google Tag Manager).
Setup steps shown in the console:
- Enable this toggle.
- Map your Analytics category (maps to
analytics_storage). - Map your Marketing category (maps to
ad_storage,ad_user_data, andad_personalization). - Publish and verify consent updates in GA4 DebugView.
What you observe: when window.gtag exists at load time, the modal calls gtag("consent", "default", ...) with all four consent types set to denied (or granted when the banner uses the opt-out consent model) and wait_for_update: 500. After each decision it calls gtag("consent", "update", ...) for the mapped categories only. If gtag is not present when the modal loads, the defaults are never set; updates still run later if gtag exists by then. The modal never loads gtag.js for you.
Tag Manager Integration
"Sends consent updates to your tag manager."
What it is: pushes a neostra_consent_update event to window.dataLayer whenever the user saves their choices, including per-category consent state in the payload.
Enable if you manage tags using Google Tag Manager, or you want tag firing to react to consent changes.
Prerequisites: the GTM container should be loaded before the banner. If window.dataLayer does not exist, the banner creates it automatically, but GTM must be present to process events.
Setup steps shown in the console:
- Enable this toggle and publish.
- In GTM, create a Custom Event trigger:
neostra_consent_update. - Use dataLayer variables in your tag conditions.
- Test in GTM preview mode.
- This does not send consent to Google directly. Use Google Consent Mode for Google tools.
What you observe: after every decision, this object is pushed to window.dataLayer. The event name is fixed and cannot be changed in the console.
{
"event": "neostra_consent_update",
"neostra_consent": {
"necessary": "granted",
"analytics": "granted",
"marketing": "denied"
},
"neostra_cp_id": "<collection-point-id>",
"neostra_subject_id": "<subject-id>",
"neostra_submitted_at": "2026-03-04T10:15:30.000Z"
}
neostra_consent has one key per category key with the value granted or denied. In GTM, create Data Layer Variables such as neostra_consent.analytics and use them in trigger conditions.
IAB TCF v2.3
"Enables consent sharing with ad partners for programmatic advertising in the EU and UK."
What it is: exposes window.__tcfapi for TCF-compliant vendors and stores consent in the euconsent-v2 cookie as a TC string.
Enable if you run programmatic ads in the EU or UK, or your ad partners require a TC string.
Prerequisites: your ad stack must support TCF, and you must provide your publisher country.
Setup steps shown in the console:
- Enable this toggle.
- Set your Publisher country (ISO 3166-1 alpha-2, for example
DE,IN,GB). - Publish and validate using the IAB Europe TCF resources.
- Not required for sites that do not use programmatic advertising.
What you observe: on load the modal installs window.__tcfapi and the __tcfapiLocator frame, so vendors can call it before the visitor has decided. Supported commands are ping, getTCData, addEventListener, and removeEventListener. After each decision the modal builds a TC string, writes it to the euconsent-v2 cookie (365 days, SameSite=Lax), and calls every registered listener with eventStatus: "useractioncomplete". Before a decision, addEventListener fires once with eventStatus: "cmpuishown".
Purpose consents come from the consent roles the owner set on each category: Functional maps to purpose 2, Analytics to 7 and 8, Advertising to 3 and 4, Data sharing to 1, Essential to no purpose. The TC string is service-specific and carries purpose consents only; vendor consents are not populated. gdprApplies is always reported as true.
window.__tcfapi("addEventListener", 2, (tcData, success) => {
if (success && tcData.eventStatus === "useractioncomplete") {
// tcData.tcString, tcData.purpose.consents
}
});
Global Privacy Control
Global Privacy Control is not a card on Signals and has no toggle. The modal always reads navigator.globalPrivacyControl and records it as receipt.compliance.gpcSignal on every decision. It does not change the visitor's choices or block anything based on the signal. If you want to honour GPC on your side, read the flag from the consent event and act accordingly.
Blocking your own tags
When Auto-Blocker is enabled and the banner does not use the opt-out consent model, the modal holds back resources until the visitor accepts the category they belong to. This is how it actually behaves:
- It watches
<script src>,<img src>, and<iframe src>elements, both those present when it starts and those added to the page later. - Any such element whose hostname is not currently allowed has its
srcmoved todata-neo-block-src. The blocker does not distinguish first-party from third-party: a relativesrcresolves to your own hostname, which is not allowed until a matching service is registered. Mark your own essential scripts, images, and iframes withdata-neo-ignore. - Inline scripts (without
src) are never touched. - After a decision, the hosts of the tracking services in every accepted category are allowed, and held-back elements whose hostname matches exactly are released. Matching is on the full hostname; subdomains are not implied.
- If the visitor accepted only some services in a category, only those hosts are allowed.
- Required categories are always accepted, so their services are released even after Reject All.
- On a repeat visit with a stored decision, the same release happens on load.
- Blocking starts only after the configuration has loaded. A script that already executed by then cannot be un-run, while an image or iframe that already loaded is unloaded when its
srcis removed. Keep the modal script first in<head>so it observes elements as early as possible.
If the visitor accepts a category that has no tracking services configured, the auto-blocker cannot tell which hosts belong to it and releases everything, logging a console warning. Register at least one service with the correct host for every category that will be accepted, including the required category.
To gate one of your own third-party scripts: make sure its hostname is registered as a tracking service in the right category on the collection point, load it with a normal <script src> tag placed after the modal script, and republish the collection point if you added the service.
The only attributes the auto-blocker recognises are data-neo-ignore (never block this element) and data-neo-block-src (set by the blocker itself; do not add it yourself).
Reacting to consent in your code
The cpmp-consent-change event
Every time the visitor saves a decision (Accept All, Reject All, or Save in the customize view), the modal dispatches cpmp-consent-change on document. This is the primary hook for your own code.
document.addEventListener("cpmp-consent-change", (event) => {
const { consentState } = event.detail;
const accepted = new Set(
consentState.receipt.choices
.filter((choice) => choice.value === "ACCEPT")
.map((choice) => choice.key)
);
if (accepted.has("analytics")) {
// load analytics
}
if (accepted.has("marketing")) {
// load marketing pixels
}
});
The event fires after the cookie is written, the auto-blocker is updated, the signals are sent, and the receipt is submitted. It does not fire on page load for an existing decision; use onConsentChange in manual initialization for that, or read the cookie.
The modal also dispatches cpmp-modal-show, cpmp-modal-hide, cpmp-customize-show, and cpmp-customize-hide on document when the banner or the customize view opens or closes. Each carries detail: { timestamp }.
Consent state shape
event.detail.consentState (and the decoded cookie value) has this shape:
{
"subjectId": "3f9c1c9e-6b1e-4f7a-9d4b-2f8e1c1a5b77",
"cpId": "<collection-point-id>",
"source": "MODAL",
"meta": {
"submittedAt": "2026-03-04T10:15:30.000Z",
"configVersion": "<configuration fingerprint>"
},
"receipt": {
"compliance": {
"gpcSignal": false,
"regulationId": "<regulation-id>",
"regulationName": "GDPR"
},
"choices": [
{ "type": "category", "id": "<category-id>", "key": "necessary", "value": "ACCEPT" },
{ "type": "category", "id": "<category-id>", "key": "analytics", "value": "ACCEPT" },
{
"type": "category",
"id": "<category-id>",
"key": "marketing",
"value": "ACCEPT",
"services": [
{ "id": "<service-id>", "vendorName": "Example Ads", "host": "ads.example.com" }
]
}
],
"attributes": [
{ "key": "<attribute-key>", "value": "<collected value>" }
]
}
}
| Field | Description |
|---|---|
subjectId | Anonymous, random ID generated in the browser and reused across decisions on the same device. |
meta.configVersion | Fingerprint of the configuration in force. Used for re-solicitation. |
receipt.compliance.gpcSignal | true if the browser sent a Global Privacy Control signal. |
receipt.compliance.regulationId, regulationName | The regulation Neostra matched to the visitor's location, if any. |
receipt.choices[] | One entry per category. key is the category key defined on the collection point. value is ACCEPT or REJECT. Required categories are always ACCEPT. |
receipt.choices[].services | Present only when the visitor accepted some, but not all, services in a category. Lists the accepted services. Absent means all services in the category follow the category value. |
receipt.attributes | Present when the collection point has attributes configured under Attributes. One { key, value } entry per attribute, collected in the browser at decision time. Device, browser, and location attributes carry an object value; the others carry a string. |
JavaScript API
The script exposes a global CPMPModal. Get the singleton with CPMPModal.getInstance().
const modal = CPMPModal.getInstance();
modal.show(); // open the banner
modal.hide(); // close the banner
modal.showCustomize(); // open the customize (category) view directly
modal.getMode(); // "production" or "preview"
show, hide, and showCustomize throw if the modal has not finished initializing. Call them from a user action, not at page load.
<a href="#" onclick="CPMPModal.getInstance().showCustomize(); return false;">
Cookie settings
</a>
If Show Floating Cookie Button is enabled on the banner's Behavior tab, visitors also get a small button in the bottom-right corner whenever the modal is hidden; clicking it reopens the banner. showCustomize has no effect for a banner whose consent model is Notice Only, because that model has no customize view.
Manual initialization
The script auto-initializes from data-cp-id. If you need callbacks, load the script without data-cp-id and call initialize yourself. The script then logs data-cp-id attribute is required; that is expected. Initialization runs once per page; a second call is ignored with a console warning.
CPMPModal.getInstance().initialize({
cpId: "<your-collection-point-id>",
mode: "production", // or "preview"
onConsentChange: (consentState) => {
// called at load when a stored decision already exists
},
onError: (error) => {
console.error("Consent modal failed to start", error);
},
});
| Option | Type | Description |
|---|---|---|
cpId | string | Required. Collection point ID. |
mode | "production" or "preview" | Validation strictness and whether buttons act. Defaults to production. |
configUrl | string | Same as the config-url attribute. |
configJson | string | Same as the data-config-json attribute. |
onConsentChange | function | Called during initialization if a stored consent state is found for the current configuration version. Not called for new decisions; listen to cpmp-consent-change for those. |
onError | function | Called if configuration loading or validation fails. |
Preference center link and re-solicitation
Preference center link
If a preference center is attached to the collection point under Preference Center, the Scripts page shows its public URL next to the banner snippet. Link to it from your privacy policy or footer so known data subjects can review and change their preferences.
Independently, the banner itself can show a preference center link. The URL is the Preference Center URL on the banner's Settings tab under Consent Management > Banners, and the link text comes from the banner's text settings. When set, the banner renders the link and opens it in a new tab. Nothing is needed on your page.
Re-solicitation
Every published configuration carries a version fingerprint derived from the set of category keys and the consent model. If the visitor's stored cookie was written under a different fingerprint, the modal clears the cookie on load and shows the banner again. Text and translation edits do not change the fingerprint, so they do not trigger re-solicitation. Expect a spike in banner impressions after a publish that adds or removes a category or switches between opt-in and opt-out.